diff --git a/.gitignore b/.gitignore index 6a60304..2dd4e2c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,12 @@ dist-ssr *.sw? .env release -electron \ No newline at end of file +electron + +# Backend server data +server/data/*.db +server/data/.encryption-key +server/dist/ + + # Orchestrator internal files +.sisyphus/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 011f1ad..db3976f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,9 @@ RUN npm run build # Production stage FROM nginx:alpine +# Remove default nginx config that conflicts with ours +RUN rm -f /etc/nginx/conf.d/default.conf + # Copy built files from build stage COPY --from=build /app/dist /usr/share/nginx/html diff --git a/README.md b/README.md index 79bd711..b10b5af 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ An app for managing github starred repositories. +**[中文文档](README_zh.md)** | English + + ## ✨ Features Tired of starring everything and finding nothing? GitHub Stars Manager automatically syncs your starred repos, uses AI to summarize and categorize them, and lets you find anything with semantic search. Track releases, filter assets, and one‑click download—smarter than manual tags, simpler than GitHub. @@ -25,6 +28,7 @@ Tired of starring everything and finding nothing? GitHub Stars Manager automatic - Smart filters: match assets by keywords (e.g., dmg/mac/arm64/aarch64) - Bilingual wiki jump: deepwiki (EN) or zread (ZH) based on language - Packaged client: no environment setup required +- Optional backend: cross-device sync, CORS-free API proxying, and encrypted token storage via Express + SQLite ### Starred Repo Manager @@ -45,6 +49,13 @@ Use your own AI model API that supports OpenAI-compatible interfaces. ![SCR-20250629-qldc](upload/SCR-20250629-qldc.png) +## 🛠 Tech Stack + +- **Frontend**: React 18 + TypeScript + Tailwind CSS +- **State Management**: Zustand +- **Icons**: Lucide React + Font Awesome +- **Build Tool**: Vite + ## 👋🏻 How to Use ### 💻 Desktop Client (Recommended) @@ -58,24 +69,105 @@ https://github.com/AmintaCCCP/GithubStarsManager/releases 2. Navigate to the directory, and open a Terminal window at the downloaded folder. 3. Run `npm install` to install dependencies and `npm run dev` to build -> 💡 When running the project locally using `npm run dev`, calls to AI services and WebDAV may fail due to CORS restrictions. To avoid this issue, use the prebuilt client application or build the client yourself. +> 💡 When running the project locally using `npm run dev`, calls to AI services and WebDAV may fail due to CORS restrictions. To avoid this issue, use the prebuilt client application or build the client yourself. Alternatively, run the backend server (`cd server && npm run dev`) to proxy API calls and avoid CORS entirely. ### 🐳 Run With Docker You can also run this application using Docker. See [DOCKER.md](DOCKER.md) for detailed instructions on how to build and deploy using Docker. The Docker setup handles CORS properly and allows you to configure any AI or WebDAV service URLs directly in the application. +### 🖥️ Backend Server (Optional) + +The app works fully without a backend (pure frontend, localStorage). An optional Express + SQLite backend adds: +- **Cross-device sync**: Share data between browsers/devices +- **CORS-free proxying**: AI and WebDAV calls go through the server, avoiding browser CORS issues +- **Token security**: API keys stored encrypted on server, never exposed to browser network tab + +#### Quick Start (Docker — recommended) +```bash +docker-compose up --build +``` +Frontend on port 8080, backend on port 3000. Data persisted in a Docker volume. + +#### Manual Setup +```bash +cd server +npm install +npm run dev +``` + +#### Environment Variables +| Variable | Required | Description | +|----------|----------|-------------| +| `API_SECRET` | No | Bearer token for API authentication. If unset, auth is disabled. | +| `ENCRYPTION_KEY` | No | AES-256 key for encrypting stored secrets. Auto-generated if unset. | +| `PORT` | No | Server port (default: 3000) | + +#### Connecting Frontend to Backend +1. Open Settings panel in the app +2. Find "Backend Server" section +3. Enter API Secret (if configured) +4. Click "Test Connection" — green indicator means connected +5. Use "Sync to Backend" / "Sync from Backend" to transfer data + +## 🤖 AI Service Configuration + +The app supports multiple AI providers. Configure yours in the Settings panel: + +- **OpenAI**: GPT-3.5 / GPT-4 +- **Anthropic**: Claude +- **Ollama**: local models with no API key needed +- **Any OpenAI-compatible API**: custom endpoint + key + +Steps: open Settings, add an AI config, enter your endpoint and key, pick a model, then test the connection. -## Who it’s for +## 💾 WebDAV Backup Configuration + +Back up and sync your data via any standard WebDAV service: + +- **Jianguoyun (坚果云)**: recommended for users in China +- **Nextcloud**: self-hosted cloud storage +- **ownCloud**: enterprise-grade option +- **Any standard WebDAV server** + +Steps: open Settings, add a WebDAV config, enter the server URL, username, password, and path, test the connection, then enable auto-backup. + +## 🚀 Deployment + +The build output is a static site, so it deploys anywhere static hosting is supported: + +- **Netlify**: connect your fork, set build command `npm run build`, publish directory `dist` +- **Vercel**: same as Netlify — import repo, build runs automatically +- **GitHub Pages**: push the `dist` folder to a `gh-pages` branch +- **Cloudflare Pages**: connect repo, build command `npm run build`, output `dist` +- **Self-hosted**: serve the `dist` folder with any HTTP server (nginx, Caddy, etc.) + +For Docker deployment see the [Backend Server](#️-backend-server-optional) section above. + +## Who it's for Developers with hundreds/thousands of stars People who systematically track releases -“Lazy-efficient” users who don’t want manual tagging +"Lazy-efficient" users who don't want manual tagging ## Additional Notes -1. There is no backend for this app, so save your important data on your own. +1. The backend is optional but recommended for web deployment. Without it, all data is stored in your browser's localStorage — back up important data regularly. 2. I can't write code, this app is entirely written by the AI, mainly for my personal requirment. If you have a new feature or meet a bug, I can only try to do it, but I can't guarantee it, because it depends on the AI to do it successfully.😹 +## 🤝 Contributing + +Contributions are welcome! + +1. Fork the project +2. Create a feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## 📄 License + +MIT — see [LICENSE](LICENSE) for details. + ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=AmintaCCCP/GithubStarsManager&type=Date)](https://www.star-history.com/#AmintaCCCP/GithubStarsManager&Date) \ No newline at end of file +[![Star History Chart](https://api.star-history.com/svg?repos=AmintaCCCP/GithubStarsManager&type=Date)](https://www.star-history.com/#AmintaCCCP/GithubStarsManager&Date) diff --git a/README_zh.md b/README_zh.md index b113f03..b7abc24 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,102 +1,112 @@ -# GitHub Stars Manager +
+ +![Logo](upload/logo.png) + +# GithubStarsManager + +![100% 本地数据](https://img.shields.io/badge/数据存储-100%25本地-success?style=flat&logo=database&logoColor=white) ![AI 支持](https://img.shields.io/badge/AI-支持多模型-blue?style=flat&logo=openai&logoColor=white) ![全平台](https://img.shields.io/badge/平台-Windows%20%7C%20macOS%20%7C%20Linux-purple?style=flat&logo=electron&logoColor=white) 一个基于AI的GitHub星标仓库管理工具,帮助您更好地组织和管理您的GitHub星标项目。 -An AI-powered GitHub starred repositories management tool to help you better organize and manage your GitHub starred projects. +GithubStarsManager - AI organizes GitHub stars for easy find | Product Hunt -## 功能特性 / Features +
-### 🔐 多种登录方式 / Multiple Login Methods -- **GitHub OAuth**: 安全便捷的一键授权登录 -- **Personal Access Token**: 适合高级用户的token登录方式 +中文 | **[English](README.md)** -### 🤖 AI智能分析 / AI-Powered Analysis +## 功能特性 + +### 🤖 AI智能分析 - 自动分析仓库内容并生成中文摘要 - 智能提取项目标签和支持平台 - 基于AI的自然语言搜索功能 -### 📂 智能分类管理 / Smart Category Management +### 📂 智能分类管理 - 预设14个常用应用分类 - 支持自定义分类创建和管理 - 基于AI标签的自动分类匹配 -### 🔔 Release订阅追踪 / Release Subscription & Tracking +### ⭐ 星标仓库管理 + +自动拉取您GitHub账户下的星标仓库,通过AI自动分析并生成仓库描述、标签和分类。支持过滤、关键词搜索,快速定位任意仓库。 + +![SCR-20250629-qkjk](upload/repo.jpg) + +### 🔔 Release订阅追踪 - 订阅感兴趣仓库的Release更新 - 智能解析下载链接和支持平台 - Release时间线视图和已读状态管理 -### 🔍 强大的搜索功能 / Powerful Search Features +订阅星标仓库的发布通知,文件发布后即可快速查看和下载。 + +![SCR-20250629-qkea](upload/release.jpg) + +### 🔍 强大的搜索功能 - AI驱动的自然语言搜索 - 多维度过滤(语言、平台、标签、状态) - 高级搜索和排序选项 -### 💾 数据备份同步 / Data Backup & Sync +### 💾 数据备份同步 - WebDAV云存储备份支持 - 跨设备数据同步 - 本地数据持久化存储 -### 🎨 现代化界面 / Modern UI +### 🎨 现代化界面 - 响应式设计,支持移动端 - 深色/浅色主题切换 - 中英文双语支持 -## 技术栈 / Tech Stack +### 🖥️ 可选后端服务 +- 可选的 Express + SQLite 后端,支持跨设备数据同步 +- AI 和 WebDAV 请求通过服务器代理,避免浏览器 CORS 限制 +- API 密钥加密存储在服务器,增强安全性 + +### 🤖 自定义AI模型 + +使用您自己的AI模型API,支持OpenAI兼容接口。 -- **Frontend**: React 18 + TypeScript + Tailwind CSS -- **State Management**: Zustand -- **Icons**: Lucide React + Font Awesome -- **Build Tool**: Vite -- **Deployment**: Netlify +![SCR-20250629-qldc](upload/SCR-20250629-qldc.png) -## 快速开始 / Quick Start +## 技术栈 -### 1. 克隆项目 / Clone Repository +- **前端**: React 18 + TypeScript + Tailwind CSS +- **状态管理**: Zustand +- **图标**: Lucide React + Font Awesome +- **构建工具**: Vite +- **部署**: Netlify + +## 💻 桌面客户端(推荐) + +直接下载桌面客户端,无需配置环境: + +https://github.com/AmintaCCCP/GithubStarsManager/releases + +## 快速开始 + +### 1. 克隆项目 ```bash git clone https://github.com/AmintaCCCP/GithubStarsManager.git cd GithubStarsManager ``` -### 2. 安装依赖 / Install Dependencies +### 2. 安装依赖 ```bash npm install ``` -### 3. 配置环境变量 / Configure Environment Variables - -创建 `.env` 文件并配置以下变量: - -```env -# GitHub OAuth App配置 (可选) -REACT_APP_GITHUB_CLIENT_ID=your_github_client_id -REACT_APP_GITHUB_CLIENT_SECRET=your_github_client_secret -``` - -### 4. 启动开发服务器 / Start Development Server +### 3. 启动开发服务器 ```bash npm run dev ``` -### 5. 构建生产版本 / Build for Production +> 💡 本地使用 `npm run dev` 运行项目时,AI 服务和 WebDAV 的调用可能因浏览器 CORS 限制而失败。建议使用预编译客户端,或启动后端服务器(`cd server && npm run dev`)代理 API 请求以完全避免 CORS 问题。 + +### 4. 构建生产版本 ```bash npm run build ``` -## GitHub OAuth配置 / GitHub OAuth Setup - -如果要使用OAuth登录功能,需要在GitHub上创建OAuth App: - -1. 访问 [GitHub Developer Settings](https://github.com/settings/developers) -2. 点击 "New OAuth App" -3. 填写应用信息: - - **Application name**: GitHub Stars Manager - - **Homepage URL**: `https://your-domain.com` - - **Authorization callback URL**: `https://your-domain.com/auth/callback` -4. 获取 Client ID 和 Client Secret -5. 将它们配置到环境变量中 - -**注意**: 出于安全考虑,在生产环境中应该通过后端服务器处理OAuth token交换,而不是在前端直接使用Client Secret。 - -## AI服务配置 / AI Service Configuration +## 🤖 AI服务配置 应用支持多种AI服务提供商: @@ -111,7 +121,7 @@ npm run build 3. 选择模型 4. 测试连接 -## WebDAV备份配置 / WebDAV Backup Configuration +## 💾 WebDAV备份配置 支持多种WebDAV服务: - **坚果云**: 国内用户推荐 @@ -125,7 +135,7 @@ npm run build 3. 测试连接 4. 启用自动备份 -## 部署 / Deployment +## 🚀 部署 ### Netlify部署 1. Fork本项目到您的GitHub账户 @@ -133,8 +143,7 @@ npm run build 3. 配置构建设置: - Build command: `npm run build` - Publish directory: `dist` -4. 配置环境变量(如果使用OAuth) -5. 部署 +4. 部署 ### 其他平台 项目构建后生成静态文件,可以部署到任何静态网站托管服务: @@ -146,7 +155,53 @@ npm run build ### Docker 部署 您也可以使用 Docker 来运行此应用程序。请参阅 [DOCKER.md](DOCKER.md) 获取详细的构建和部署说明。Docker 设置正确处理了 CORS,并允许您直接在应用程序中配置任何 AI 或 WebDAV 服务 URL。 -## 贡献 / Contributing +### 🖥️ 后端服务器(可选) + +应用在没有后端的情况下也能完整运行(纯前端,使用 localStorage)。可选的 Express + SQLite 后端提供以下额外功能: + +- **跨设备同步**: 在不同浏览器和设备间共享数据 +- **无 CORS 代理**: AI 和 WebDAV 请求通过服务器转发,避免浏览器 CORS 限制 +- **令牌安全**: API 密钥加密存储在服务器,不会暴露在浏览器网络请求中 + +#### 快速启动(推荐使用 Docker) +```bash +docker-compose up --build +``` +前端运行在 8080 端口,后端运行在 3000 端口。数据持久化存储在 Docker 卷中。 + +#### 手动启动 +```bash +cd server +npm install +npm run dev +``` + +#### 环境变量 +| 变量 | 必填 | 说明 | +|----------|----------|-------------| +| `API_SECRET` | 否 | API 认证令牌。未设置时禁用认证。 | +| `ENCRYPTION_KEY` | 否 | 用于加密存储密钥的 AES-256 密钥。未设置时自动生成。 | +| `PORT` | 否 | 服务器端口(默认:3000) | + +#### 前端连接后端 +1. 打开应用中的设置面板 +2. 找到「后端服务器」部分 +3. 输入 API Secret(如已配置) +4. 点击「测试连接」,绿色指示灯表示连接成功 +5. 使用「同步到后端」/「从后端同步」来传输数据 + +## 目标用户 + +- 拥有数百甚至数千星标的开发者 +- 系统性追踪软件发布的用户 +- 不想手动打标签的「懒效率」用户 + +## 补充说明 + +1. 后端为可选项,但对于网页部署推荐启用。不启用时,所有数据存储在浏览器 localStorage 中,请定期备份重要数据。 +2. 我不会写代码,这个应用完全由AI编写,主要满足我个人需求。如果您有新功能需求或遇到Bug,我只能尽力尝试,但无法保证成功,因为这取决于AI能否完成。😹 + +## 贡献 欢迎提交Issue和Pull Request! @@ -156,18 +211,22 @@ npm run build 4. 推送到分支 (`git push origin feature/AmazingFeature`) 5. 开启Pull Request -## 许可证 / License +## 许可证 本项目采用MIT许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 -## 支持 / Support +## 支持 如果您觉得这个项目有用,请给它一个⭐️! 如有问题或建议,请提交Issue或联系作者。 +## 星标历史 + +[![Star History Chart](https://api.star-history.com/svg?repos=AmintaCCCP/GithubStarsManager&type=Date)](https://www.star-history.com/#AmintaCCCP/GithubStarsManager&Date) + --- -**Live Demo**: [https://soft-stroopwafel-2b73d1.netlify.app](https://soft-stroopwafel-2b73d1.netlify.app) +**在线演示**: [https://soft-stroopwafel-2b73d1.netlify.app](https://soft-stroopwafel-2b73d1.netlify.app) -**GitHub Repository**: [https://github.com/AmintaCCCP/GithubStarsManager](https://github.com/AmintaCCCP/GithubStarsManager) \ No newline at end of file +**GitHub 仓库**: [https://github.com/AmintaCCCP/GithubStarsManager](https://github.com/AmintaCCCP/GithubStarsManager) diff --git a/dist/index.html b/dist/index.html index ebac633..61c41c1 100644 --- a/dist/index.html +++ b/dist/index.html @@ -10,8 +10,8 @@ - - + +
diff --git a/docker-compose.yml b/docker-compose.yml index 1d964c8..85c99be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,24 @@ version: '3.8' services: - github-stars-manager: + frontend: build: . ports: - "8080:80" + depends_on: + - backend restart: unless-stopped - # Environment variables can be set here if needed - # environment: - # - NODE_ENV=production \ No newline at end of file + + backend: + build: ./server + expose: + - "3000" + environment: + - API_SECRET=${API_SECRET:-} + - ENCRYPTION_KEY=${ENCRYPTION_KEY:-} + volumes: + - backend-data:/app/data + restart: unless-stopped + +volumes: + backend-data: \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index 76c3173..084e279 100644 --- a/nginx.conf +++ b/nginx.conf @@ -30,6 +30,19 @@ http { server { listen 80; server_name localhost; + # Backend API proxy + location /api/ { + proxy_pass http://backend:3000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_send_timeout 120s; + client_max_body_size 50m; + } + # Handle preflight requests for CORS location / { diff --git a/package-lock.json b/package-lock.json index b973ef2..88f1197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-stars-manager", - "version": "0.1.3", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-stars-manager", - "version": "0.1.3", + "version": "0.1.8", "dependencies": { "date-fns": "^3.3.1", "lucide-react": "^0.344.0", @@ -21,6 +21,7 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.18", + "concurrently": "^8.2.0", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", @@ -294,6 +295,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", @@ -1832,6 +1843,120 @@ "node": ">= 6" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1862,6 +1987,143 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2444,6 +2706,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2783,6 +3055,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3356,6 +3635,16 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3450,6 +3739,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -3495,6 +3794,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3516,6 +3828,12 @@ "node": ">=0.10.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3755,6 +4073,16 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -3773,6 +4101,13 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4103,6 +4438,16 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -4121,6 +4466,80 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4162,4 +4581,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 1b61c3f..c8ebacb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "electron": "electron electron/main.js", "electron:dev": "NODE_ENV=development electron electron/main.js", "dist": "electron-builder", - "update-version": "node scripts/update-version.cjs" + "update-version": "node scripts/update-version.cjs", + "dev:server": "cd server && npm run dev", + "dev:all": "concurrently \"npm run dev\" \"npm run dev:server\"", + "build:server": "cd server && npm run build", + "build:all": "npm run build && npm run build:server" }, "dependencies": { "date-fns": "^3.3.1", @@ -37,6 +41,7 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "concurrently": "^8.2.0" } } diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..abd944c --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +data/*.db +data/.encryption-key diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..01d4b26 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build && npm prune --omit=dev + +FROM node:22-alpine +WORKDIR /app +COPY --from=build /app/dist ./dist +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ +RUN mkdir -p data && chown -R node:node /app +USER node +VOLUME /app/data +EXPOSE 3000 +CMD ["node", "dist/index.js"] diff --git a/server/data/.gitkeep b/server/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..32a2bd5 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,4146 @@ +{ + "name": "github-stars-manager-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-stars-manager-server", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^11.0.0", + "cors": "^2.8.5", + "express": "^4.21.0", + "helmet": "^7.1.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.8", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/supertest": "^6.0.2", + "supertest": "^6.3.4", + "tsx": "^4.7.0", + "typescript": "^5.5.3", + "vitest": "^1.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..eb19c2e --- /dev/null +++ b/server/package.json @@ -0,0 +1,31 @@ +{ + "name": "github-stars-manager-server", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "express": "^4.21.0", + "better-sqlite3": "^11.0.0", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/better-sqlite3": "^7.6.8", + "@types/cors": "^2.8.17", + "@types/morgan": "^1.9.9", + "tsx": "^4.7.0", + "typescript": "^5.5.3", + "vitest": "^1.6.0", + "supertest": "^6.3.4", + "@types/supertest": "^6.0.2" + } +} diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 0000000..de13a10 --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,50 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +interface Config { + port: number; + apiSecret: string | null; + encryptionKey: string; + dbPath: string; + nodeEnv: string; +} + +function resolveDataDir(): string { + const dataDir = path.resolve(process.cwd(), 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + return dataDir; +} + +function resolveEncryptionKey(dataDir: string): string { + const envKey = process.env.ENCRYPTION_KEY; + if (envKey) { + return envKey; + } + + const keyFilePath = path.join(dataDir, '.encryption-key'); + if (fs.existsSync(keyFilePath)) { + return fs.readFileSync(keyFilePath, 'utf-8').trim(); + } + + const newKey = crypto.randomBytes(32).toString('hex'); + fs.writeFileSync(keyFilePath, newKey, { mode: 0o600 }); + console.log('Generated new encryption key and saved to data/.encryption-key'); + return newKey; +} + +function loadConfig(): Config { + const dataDir = resolveDataDir(); + + return { + port: parseInt(process.env.PORT || '3000', 10), + apiSecret: process.env.API_SECRET || null, + encryptionKey: resolveEncryptionKey(dataDir), + dbPath: process.env.DB_PATH || path.join(dataDir, 'data.db'), + nodeEnv: process.env.NODE_ENV || 'development', + }; +} + +export const config = loadConfig(); diff --git a/server/src/db/connection.ts b/server/src/db/connection.ts new file mode 100644 index 0000000..5ea74bd --- /dev/null +++ b/server/src/db/connection.ts @@ -0,0 +1,29 @@ +import Database from 'better-sqlite3'; +import fs from 'node:fs'; +import path from 'node:path'; +import { config } from '../config.js'; + +let db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (db) return db; + + // Ensure directory exists + const dir = path.dirname(config.dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + db = new Database(config.dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + + return db; +} + +export function closeDb(): void { + if (db) { + db.close(); + db = null; + } +} diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts new file mode 100644 index 0000000..5d65e69 --- /dev/null +++ b/server/src/db/migrations.ts @@ -0,0 +1,43 @@ +import type Database from 'better-sqlite3'; +import { initializeSchema } from './schema.js'; + +const migrations: Record void> = { + 1: (db) => { + initializeSchema(db); + }, +}; + +export function runMigrations(db: Database.Database): void { + // Ensure schema_version table exists first + db.exec(` + CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + `); + + const currentVersionRow = db + .prepare('SELECT MAX(version) as version FROM schema_version') + .get() as { version: number | null } | undefined; + + const currentVersion = currentVersionRow?.version ?? 0; + const targetVersion = Math.max(...Object.keys(migrations).map(Number)); + + if (currentVersion >= targetVersion) { + return; + } + + const applyMigration = db.transaction(() => { + for (let v = currentVersion + 1; v <= targetVersion; v++) { + const migration = migrations[v]; + if (migration) { + console.log(`Applying migration v${v}...`); + migration(db); + db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(v); + console.log(`Migration v${v} applied.`); + } + } + }); + + applyMigration(); +} diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts new file mode 100644 index 0000000..2f31e4e --- /dev/null +++ b/server/src/db/schema.ts @@ -0,0 +1,95 @@ +import type Database from 'better-sqlite3'; + +export function initializeSchema(db: Database.Database): void { + db.exec(` + CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS repositories ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + full_name TEXT NOT NULL UNIQUE, + description TEXT, + html_url TEXT NOT NULL, + stargazers_count INTEGER DEFAULT 0, + language TEXT, + created_at TEXT, + updated_at TEXT, + pushed_at TEXT, + starred_at TEXT, + owner_login TEXT NOT NULL, + owner_avatar_url TEXT, + topics TEXT, + ai_summary TEXT, + ai_tags TEXT, + ai_platforms TEXT, + analyzed_at TEXT, + analysis_failed INTEGER DEFAULT 0, + custom_description TEXT, + custom_tags TEXT, + custom_category TEXT, + last_edited TEXT, + subscribed_to_releases INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS releases ( + id INTEGER PRIMARY KEY, + tag_name TEXT NOT NULL, + name TEXT, + body TEXT, + published_at TEXT, + html_url TEXT, + assets TEXT, + repo_id INTEGER NOT NULL, + repo_full_name TEXT NOT NULL, + repo_name TEXT NOT NULL, + prerelease INTEGER DEFAULT 0, + draft INTEGER DEFAULT 0, + is_read INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS categories ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + icon TEXT NOT NULL DEFAULT '📁', + keywords TEXT, + is_custom INTEGER DEFAULT 1 + ); + + CREATE TABLE IF NOT EXISTS ai_configs ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + api_type TEXT DEFAULT 'openai', + base_url TEXT NOT NULL, + api_key_encrypted TEXT NOT NULL, + model TEXT NOT NULL, + is_active INTEGER DEFAULT 0, + custom_prompt TEXT, + use_custom_prompt INTEGER DEFAULT 0, + concurrency INTEGER DEFAULT 1 + ); + + CREATE TABLE IF NOT EXISTS webdav_configs ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL, + username TEXT NOT NULL, + password_encrypted TEXT NOT NULL, + path TEXT NOT NULL DEFAULT '/', + is_active INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS asset_filters ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + keywords TEXT + ); + + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ); + `); +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..42ba420 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,82 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import { config } from './config.js'; +import { authMiddleware } from './middleware/auth.js'; +import { errorHandler } from './middleware/errorHandler.js'; +import { getDb, closeDb } from './db/connection.js'; +import { runMigrations } from './db/migrations.js'; +import healthRouter from './routes/health.js'; +import repositoriesRouter from './routes/repositories.js'; +import releasesRouter from './routes/releases.js'; +import categoriesRouter from './routes/categories.js'; +import configsRouter from './routes/configs.js'; +import syncRouter from './routes/sync.js'; +import proxyRouter from './routes/proxy.js'; + +export function createApp(): express.Express { + const app = express(); + + // Middleware + app.use(helmet()); + app.use(cors()); + app.use(morgan('combined')); + app.use(express.json({ limit: '50mb' })); + + // Auth middleware for all /api/* except /api/health + app.use('/api', authMiddleware); + + // Routes + app.use(healthRouter); + + // Wave 2: Data CRUD routes + app.use(repositoriesRouter); + app.use(releasesRouter); + app.use(categoriesRouter); + app.use(configsRouter); + app.use(syncRouter); + + // Wave 3: Proxy routes + app.use(proxyRouter); + + // Global error handler + app.use(errorHandler); + + return app; +} + +function startServer(): void { + // Initialize database + const db = getDb(); + runMigrations(db); + console.log('✅ Database initialized'); + + const app = createApp(); + + const server = app.listen(config.port, () => { + console.log(`🚀 Server running on port ${config.port}`); + if (!config.apiSecret) { + console.warn('⚠️ Running without API_SECRET — auth is disabled'); + } + }); + + // Graceful shutdown + const shutdown = () => { + console.log('\n🛑 Shutting down...'); + server.close(() => { + closeDb(); + console.log('👋 Server stopped'); + process.exit(0); + }); + }; + + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); +} + +// Only start server when run directly (not imported for tests) +const isMainModule = process.argv[1] && new URL(import.meta.url).pathname === new URL(`file://${process.argv[1]}`).pathname; +if (isMainModule) { + startServer(); +} diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts new file mode 100644 index 0000000..3487215 --- /dev/null +++ b/server/src/middleware/auth.ts @@ -0,0 +1,42 @@ +import crypto from 'node:crypto'; +import type { Request, Response, NextFunction } from 'express'; +import { config } from '../config.js'; + +let warnedOnce = false; + +export function authMiddleware(req: Request, res: Response, next: NextFunction): void { + // Skip auth for health check + if (req.method === 'GET' && req.path === '/health') { + next(); + return; + } + + // Dev mode: no API_SECRET set + if (!config.apiSecret) { + if (!warnedOnce) { + console.warn('⚠️ API_SECRET not set — auth disabled (dev mode)'); + warnedOnce = true; + } + next(); + return; + } + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ error: 'Unauthorized', code: 'UNAUTHORIZED' }); + return; + } + + const token = authHeader.slice(7); + + // Constant-time comparison + const tokenBuf = Buffer.from(token); + const secretBuf = Buffer.from(config.apiSecret); + + if (tokenBuf.length !== secretBuf.length || !crypto.timingSafeEqual(tokenBuf, secretBuf)) { + res.status(401).json({ error: 'Unauthorized', code: 'UNAUTHORIZED' }); + return; + } + + next(); +} diff --git a/server/src/middleware/errorHandler.ts b/server/src/middleware/errorHandler.ts new file mode 100644 index 0000000..4386201 --- /dev/null +++ b/server/src/middleware/errorHandler.ts @@ -0,0 +1,24 @@ +import type { Request, Response, NextFunction } from 'express'; + +export function errorHandler( + err: Error, + _req: Request, + res: Response, + _next: NextFunction +): void { + console.error('Unhandled error:', err.stack || err.message); + + if (res.headersSent) { + return _next(err); + } + + const errWithMeta = err as Error & { statusCode?: number; code?: string }; + const statusCode = errWithMeta.statusCode || 500; + const message = + process.env.NODE_ENV === 'production' + ? 'Internal Server Error' + : err.message || 'Internal Server Error'; + const code = errWithMeta.code || 'INTERNAL_SERVER_ERROR'; + + res.status(statusCode).json({ error: message, code }); +} \ No newline at end of file diff --git a/server/src/routes/categories.ts b/server/src/routes/categories.ts new file mode 100644 index 0000000..2943499 --- /dev/null +++ b/server/src/routes/categories.ts @@ -0,0 +1,194 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; + +const router = Router(); + +function parseJsonColumn(value: unknown): unknown[] { + if (typeof value !== 'string' || !value) return []; + try { return JSON.parse(value); } catch { return []; } +} + +// ── Categories ── + +function transformCategory(row: Record) { + return { + id: row.id, + name: row.name, + description: row.description, + keywords: parseJsonColumn(row.keywords), + color: row.color, + icon: row.icon, + sort_order: row.sort_order, + }; +} + +// GET /api/categories +router.get('/api/categories', (_req, res) => { + try { + const db = getDb(); + const rows = db.prepare('SELECT * FROM categories ORDER BY sort_order ASC, name ASC').all() as Record[]; + res.json(rows.map(transformCategory)); + } catch (err) { + console.error('GET /api/categories error:', err); + res.status(500).json({ error: 'Failed to fetch categories', code: 'FETCH_CATEGORIES_FAILED' }); + } +}); + +// POST /api/categories +router.post('/api/categories', (req, res) => { + try { + const db = getDb(); + const { name, description, keywords, color, icon, sort_order } = req.body as Record; + + const result = db.prepare( + 'INSERT INTO categories (name, description, keywords, color, icon, sort_order) VALUES (?, ?, ?, ?, ?, ?)' + ).run( + name ?? '', description ?? null, + JSON.stringify(keywords ?? []), + color ?? null, icon ?? null, sort_order ?? 0 + ); + + const row = db.prepare('SELECT * FROM categories WHERE id = ?').get(result.lastInsertRowid) as Record; + res.status(201).json(transformCategory(row)); + } catch (err) { + console.error('POST /api/categories error:', err); + res.status(500).json({ error: 'Failed to create category', code: 'CREATE_CATEGORY_FAILED' }); + } +}); + +// PUT /api/categories/:id +router.put('/api/categories/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const { name, description, keywords, color, icon, sort_order } = req.body as Record; + + db.prepare( + 'UPDATE categories SET name = ?, description = ?, keywords = ?, color = ?, icon = ?, sort_order = ? WHERE id = ?' + ).run( + name ?? '', description ?? null, + JSON.stringify(keywords ?? []), + color ?? null, icon ?? null, sort_order ?? 0, id + ); + + const row = db.prepare('SELECT * FROM categories WHERE id = ?').get(id) as Record | undefined; + if (!row) { + res.status(404).json({ error: 'Category not found', code: 'CATEGORY_NOT_FOUND' }); + return; + } + res.json(transformCategory(row)); + } catch (err) { + console.error('PUT /api/categories error:', err); + res.status(500).json({ error: 'Failed to update category', code: 'UPDATE_CATEGORY_FAILED' }); + } +}); + +// DELETE /api/categories/:id +router.delete('/api/categories/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const result = db.prepare('DELETE FROM categories WHERE id = ?').run(id); + if (result.changes === 0) { + res.status(404).json({ error: 'Category not found', code: 'CATEGORY_NOT_FOUND' }); + return; + } + res.json({ deleted: true }); + } catch (err) { + console.error('DELETE /api/categories error:', err); + res.status(500).json({ error: 'Failed to delete category', code: 'DELETE_CATEGORY_FAILED' }); + } +}); + +// ── Asset Filters ── + +function transformAssetFilter(row: Record) { + return { + id: row.id, + name: row.name, + description: row.description, + keywords: parseJsonColumn(row.keywords), + platform: row.platform, + sort_order: row.sort_order, + }; +} + +// GET /api/asset-filters +router.get('/api/asset-filters', (_req, res) => { + try { + const db = getDb(); + const rows = db.prepare('SELECT * FROM asset_filters ORDER BY sort_order ASC, name ASC').all() as Record[]; + res.json(rows.map(transformAssetFilter)); + } catch (err) { + console.error('GET /api/asset-filters error:', err); + res.status(500).json({ error: 'Failed to fetch asset filters', code: 'FETCH_ASSET_FILTERS_FAILED' }); + } +}); + +// POST /api/asset-filters +router.post('/api/asset-filters', (req, res) => { + try { + const db = getDb(); + const { name, description, keywords, platform, sort_order } = req.body as Record; + + const result = db.prepare( + 'INSERT INTO asset_filters (name, description, keywords, platform, sort_order) VALUES (?, ?, ?, ?, ?)' + ).run( + name ?? '', description ?? null, + JSON.stringify(keywords ?? []), + platform ?? null, sort_order ?? 0 + ); + + const row = db.prepare('SELECT * FROM asset_filters WHERE id = ?').get(result.lastInsertRowid) as Record; + res.status(201).json(transformAssetFilter(row)); + } catch (err) { + console.error('POST /api/asset-filters error:', err); + res.status(500).json({ error: 'Failed to create asset filter', code: 'CREATE_ASSET_FILTER_FAILED' }); + } +}); + +// PUT /api/asset-filters/:id +router.put('/api/asset-filters/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const { name, description, keywords, platform, sort_order } = req.body as Record; + + db.prepare( + 'UPDATE asset_filters SET name = ?, description = ?, keywords = ?, platform = ?, sort_order = ? WHERE id = ?' + ).run( + name ?? '', description ?? null, + JSON.stringify(keywords ?? []), + platform ?? null, sort_order ?? 0, id + ); + + const row = db.prepare('SELECT * FROM asset_filters WHERE id = ?').get(id) as Record | undefined; + if (!row) { + res.status(404).json({ error: 'Asset filter not found', code: 'ASSET_FILTER_NOT_FOUND' }); + return; + } + res.json(transformAssetFilter(row)); + } catch (err) { + console.error('PUT /api/asset-filters error:', err); + res.status(500).json({ error: 'Failed to update asset filter', code: 'UPDATE_ASSET_FILTER_FAILED' }); + } +}); + +// DELETE /api/asset-filters/:id +router.delete('/api/asset-filters/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const result = db.prepare('DELETE FROM asset_filters WHERE id = ?').run(id); + if (result.changes === 0) { + res.status(404).json({ error: 'Asset filter not found', code: 'ASSET_FILTER_NOT_FOUND' }); + return; + } + res.json({ deleted: true }); + } catch (err) { + console.error('DELETE /api/asset-filters error:', err); + res.status(500).json({ error: 'Failed to delete asset filter', code: 'DELETE_ASSET_FILTER_FAILED' }); + } +}); + +export default router; diff --git a/server/src/routes/configs.ts b/server/src/routes/configs.ts new file mode 100644 index 0000000..f3b79fa --- /dev/null +++ b/server/src/routes/configs.ts @@ -0,0 +1,418 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; +import { encrypt, decrypt } from '../services/crypto.js'; +import { config } from '../config.js'; + +const router = Router(); + +// ── AI Configs ── + +function maskApiKey(key: string | null | undefined): string { + if (!key || typeof key !== 'string') return ''; + if (key.length <= 4) return '****'; + return '***' + key.slice(-4); +} + +// GET /api/configs/ai +router.get('/api/configs/ai', (req, res) => { + try { + const db = getDb(); + const shouldDecrypt = req.query.decrypt === 'true'; + const rows = db.prepare('SELECT * FROM ai_configs ORDER BY id ASC').all() as Record[]; + const configs = rows.map((row) => { + let decryptedKey = ''; + try { + if (row.api_key_encrypted && typeof row.api_key_encrypted === 'string') { + decryptedKey = decrypt(row.api_key_encrypted, config.encryptionKey); + } + } catch { /* leave empty */ } + return { + id: row.id, + name: row.name, + apiType: row.api_type, + model: row.model, + baseUrl: row.base_url, + apiKey: shouldDecrypt ? decryptedKey : maskApiKey(decryptedKey), + isActive: !!row.is_active, + customPrompt: row.custom_prompt ?? null, + useCustomPrompt: !!row.use_custom_prompt, + concurrency: row.concurrency ?? 1, + }; + }); + res.json(configs); + } catch (err) { + console.error('GET /api/configs/ai error:', err); + res.status(500).json({ error: 'Failed to fetch AI configs', code: 'FETCH_AI_CONFIGS_FAILED' }); + } +}); + +// POST /api/configs/ai +router.post('/api/configs/ai', (req, res) => { + try { + const db = getDb(); + const { name, apiType, model, baseUrl, apiKey, isActive, customPrompt, useCustomPrompt, concurrency } = req.body as Record; + + const encryptedKey = apiKey && typeof apiKey === 'string' ? encrypt(apiKey, config.encryptionKey) : null; + + const result = db.prepare( + 'INSERT INTO ai_configs (name, api_type, model, base_url, api_key_encrypted, is_active, custom_prompt, use_custom_prompt, concurrency) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' + ).run( + name ?? '', apiType ?? 'openai', model ?? '', baseUrl ?? null, + encryptedKey, isActive ? 1 : 0, customPrompt ?? null, useCustomPrompt ? 1 : 0, concurrency ?? 1 + ); + + res.status(201).json({ id: result.lastInsertRowid, name, apiType, model, baseUrl, apiKey: maskApiKey(apiKey as string), isActive: !!isActive }); + } catch (err) { + console.error('POST /api/configs/ai error:', err); + res.status(500).json({ error: 'Failed to create AI config', code: 'CREATE_AI_CONFIG_FAILED' }); + } +}); + +// PUT /api/configs/ai/bulk — replace all AI configs (for sync) +// MUST be registered before :id route to avoid matching 'bulk' as an id +router.put('/api/configs/ai/bulk', (req, res) => { + try { + const db = getDb(); + const configs = req.body.configs as Array<{ + id: string; + name: string; + apiType?: string; + baseUrl: string; + apiKey: string; + model: string; + isActive: boolean; + customPrompt?: string; + useCustomPrompt?: boolean; + concurrency?: number; + }>; + + if (!Array.isArray(configs)) { + res.status(400).json({ error: 'configs array required', code: 'INVALID_REQUEST' }); + return; + } + + const bulkSync = db.transaction(() => { + // Read existing keys BEFORE delete + const existingKeys = new Map(); + const existingRows = db.prepare('SELECT id, api_key_encrypted FROM ai_configs').all() as Array<{ id: string; api_key_encrypted: string }>; + for (const row of existingRows) { + if (row.api_key_encrypted) existingKeys.set(String(row.id), row.api_key_encrypted); + } + + db.prepare('DELETE FROM ai_configs').run(); + + const stmt = db.prepare(` + INSERT INTO ai_configs (id, name, api_type, base_url, api_key_encrypted, model, is_active, custom_prompt, use_custom_prompt, concurrency) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + for (const c of configs) { + let encryptedKey = ''; + if (c.apiKey && !c.apiKey.startsWith('***')) { + encryptedKey = encrypt(c.apiKey, config.encryptionKey); + } else { + encryptedKey = existingKeys.get(String(c.id)) ?? ''; + } + stmt.run( + c.id, c.name ?? '', c.apiType ?? 'openai', c.baseUrl ?? '', + encryptedKey, c.model ?? '', c.isActive ? 1 : 0, + c.customPrompt ?? null, c.useCustomPrompt ? 1 : 0, c.concurrency ?? 1 + ); + } + }); + + bulkSync(); + res.json({ synced: configs.length }); + } catch (err) { + console.error('PUT /api/configs/ai/bulk error:', err); + res.status(500).json({ error: 'Failed to sync AI configs', code: 'SYNC_AI_CONFIGS_FAILED' }); + } +}); + +// PUT /api/configs/ai/:id +router.put('/api/configs/ai/:id', (req, res) => { + try { + const db = getDb(); + const id = req.params.id; + const { name, apiType, model, baseUrl, apiKey, isActive, customPrompt, useCustomPrompt, concurrency } = req.body as Record; + + let encryptedKey: string | null = null; + if (apiKey && typeof apiKey === 'string' && !apiKey.startsWith('***')) { + encryptedKey = encrypt(apiKey, config.encryptionKey); + } else { + // Keep existing encrypted key + const existing = db.prepare('SELECT api_key_encrypted FROM ai_configs WHERE id = ?').get(id) as Record | undefined; + encryptedKey = (existing?.api_key_encrypted as string) ?? null; + } + + const result = db.prepare( + 'UPDATE ai_configs SET name = ?, api_type = ?, model = ?, base_url = ?, api_key_encrypted = ?, is_active = ?, custom_prompt = ?, use_custom_prompt = ?, concurrency = ? WHERE id = ?' + ).run(name ?? '', apiType ?? 'openai', model ?? '', baseUrl ?? null, encryptedKey, isActive ? 1 : 0, customPrompt ?? null, useCustomPrompt ? 1 : 0, concurrency ?? 1, id); + + if (result.changes === 0) { + res.status(404).json({ error: 'AI config not found', code: 'AI_CONFIG_NOT_FOUND' }); + return; + } + let maskedKey = ''; + if (encryptedKey) { + try { maskedKey = maskApiKey(decrypt(encryptedKey, config.encryptionKey)); } catch { maskedKey = '****'; } + } + + res.json({ id, name, apiType, model, baseUrl, apiKey: maskedKey, isActive: !!isActive }); + } catch (err) { + console.error('PUT /api/configs/ai error:', err); + res.status(500).json({ error: 'Failed to update AI config', code: 'UPDATE_AI_CONFIG_FAILED' }); + } +}); + +// DELETE /api/configs/ai/:id +router.delete('/api/configs/ai/:id', (req, res) => { + try { + const db = getDb(); + const id = req.params.id; + const result = db.prepare('DELETE FROM ai_configs WHERE id = ?').run(id); + if (result.changes === 0) { + res.status(404).json({ error: 'AI config not found', code: 'AI_CONFIG_NOT_FOUND' }); + return; + } + res.json({ deleted: true }); + } catch (err) { + console.error('DELETE /api/configs/ai error:', err); + res.status(500).json({ error: 'Failed to delete AI config', code: 'DELETE_AI_CONFIG_FAILED' }); + } +}); + +// ── WebDAV Configs ── + +function maskPassword(pwd: string | null | undefined): string { + if (!pwd || typeof pwd !== 'string') return ''; + if (pwd.length <= 4) return '****'; + return '***' + pwd.slice(-4); +} + +// GET /api/configs/webdav +router.get('/api/configs/webdav', (req, res) => { + try { + const db = getDb(); + const shouldDecrypt = req.query.decrypt === 'true'; + const rows = db.prepare('SELECT * FROM webdav_configs ORDER BY id ASC').all() as Record[]; + const configs = rows.map((row) => { + let decryptedPwd = ''; + try { + if (row.password_encrypted && typeof row.password_encrypted === 'string') { + decryptedPwd = decrypt(row.password_encrypted, config.encryptionKey); + } + } catch { /* leave empty */ } + return { + id: row.id, + name: row.name, + url: row.url, + username: row.username, + password: shouldDecrypt ? decryptedPwd : maskPassword(decryptedPwd), + path: row.path, + isActive: !!row.is_active, + }; + }); + res.json(configs); + } catch (err) { + console.error('GET /api/configs/webdav error:', err); + res.status(500).json({ error: 'Failed to fetch WebDAV configs', code: 'FETCH_WEBDAV_CONFIGS_FAILED' }); + } +}); + +// POST /api/configs/webdav +router.post('/api/configs/webdav', (req, res) => { + try { + const db = getDb(); + const { name, url, username, password, path, isActive } = req.body as Record; + + const encryptedPwd = password && typeof password === 'string' ? encrypt(password, config.encryptionKey) : null; + + const result = db.prepare( + 'INSERT INTO webdav_configs (name, url, username, password_encrypted, path, is_active) VALUES (?, ?, ?, ?, ?, ?)' + ).run( + name ?? '', url ?? '', username ?? '', encryptedPwd, + path ?? '/', isActive ? 1 : 0 + ); + + res.status(201).json({ id: result.lastInsertRowid, name, url, username, password: maskPassword(password as string), path, isActive: !!isActive }); + } catch (err) { + console.error('POST /api/configs/webdav error:', err); + res.status(500).json({ error: 'Failed to create WebDAV config', code: 'CREATE_WEBDAV_CONFIG_FAILED' }); + } +}); + +// PUT /api/configs/webdav/bulk — replace all WebDAV configs (for sync) +// MUST be registered before :id route to avoid matching 'bulk' as an id +router.put('/api/configs/webdav/bulk', (req, res) => { + try { + const db = getDb(); + const configs = req.body.configs as Array<{ + id: string; + name: string; + url: string; + username: string; + password: string; + path: string; + isActive: boolean; + }>; + + if (!Array.isArray(configs)) { + res.status(400).json({ error: 'configs array required', code: 'INVALID_REQUEST' }); + return; + } + + const bulkSync = db.transaction(() => { + // Read existing passwords BEFORE delete + const existingPwds = new Map(); + const existingRows = db.prepare('SELECT id, password_encrypted FROM webdav_configs').all() as Array<{ id: string; password_encrypted: string }>; + for (const row of existingRows) { + if (row.password_encrypted) existingPwds.set(String(row.id), row.password_encrypted); + } + + db.prepare('DELETE FROM webdav_configs').run(); + + const stmt = db.prepare(` + INSERT INTO webdav_configs (id, name, url, username, password_encrypted, path, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + for (const c of configs) { + let encryptedPwd = ''; + if (c.password && !c.password.startsWith('***')) { + encryptedPwd = encrypt(c.password, config.encryptionKey); + } else { + encryptedPwd = existingPwds.get(String(c.id)) ?? ''; + } + stmt.run( + c.id, c.name ?? '', c.url ?? '', c.username ?? '', + encryptedPwd, c.path ?? '/', c.isActive ? 1 : 0 + ); + } + }); + + bulkSync(); + res.json({ synced: configs.length }); + } catch (err) { + console.error('PUT /api/configs/webdav/bulk error:', err); + res.status(500).json({ error: 'Failed to sync WebDAV configs', code: 'SYNC_WEBDAV_CONFIGS_FAILED' }); + } +}); + +// PUT /api/configs/webdav/:id +router.put('/api/configs/webdav/:id', (req, res) => { + try { + const db = getDb(); + const id = req.params.id; + const { name, url, username, password, path, isActive } = req.body as Record; + + let encryptedPwd: string | null = null; + if (password && typeof password === 'string' && !password.startsWith('***')) { + encryptedPwd = encrypt(password, config.encryptionKey); + } else { + const existing = db.prepare('SELECT password_encrypted FROM webdav_configs WHERE id = ?').get(id) as Record | undefined; + encryptedPwd = (existing?.password_encrypted as string) ?? null; + } + + const result = db.prepare( + 'UPDATE webdav_configs SET name = ?, url = ?, username = ?, password_encrypted = ?, path = ?, is_active = ? WHERE id = ?' + ).run(name ?? '', url ?? '', username ?? '', encryptedPwd, path ?? '/', isActive ? 1 : 0, id); + + if (result.changes === 0) { + res.status(404).json({ error: 'WebDAV config not found', code: 'WEBDAV_CONFIG_NOT_FOUND' }); + return; + } + let maskedPwd = ''; + if (encryptedPwd) { + try { maskedPwd = maskPassword(decrypt(encryptedPwd, config.encryptionKey)); } catch { maskedPwd = '****'; } + } + + res.json({ id, name, url, username, password: maskedPwd, path, isActive: !!isActive }); + } catch (err) { + console.error('PUT /api/configs/webdav error:', err); + res.status(500).json({ error: 'Failed to update WebDAV config', code: 'UPDATE_WEBDAV_CONFIG_FAILED' }); + } +}); + +// DELETE /api/configs/webdav/:id +router.delete('/api/configs/webdav/:id', (req, res) => { + try { + const db = getDb(); + const id = req.params.id; + const result = db.prepare('DELETE FROM webdav_configs WHERE id = ?').run(id); + if (result.changes === 0) { + res.status(404).json({ error: 'WebDAV config not found', code: 'WEBDAV_CONFIG_NOT_FOUND' }); + return; + } + res.json({ deleted: true }); + } catch (err) { + console.error('DELETE /api/configs/webdav error:', err); + res.status(500).json({ error: 'Failed to delete WebDAV config', code: 'DELETE_WEBDAV_CONFIG_FAILED' }); + } +}); + +// ── Settings ── + +// GET /api/settings +router.get('/api/settings', (_req, res) => { + try { + const db = getDb(); + const rows = db.prepare('SELECT * FROM settings').all() as Record[]; + const settings: Record = {}; + + for (const row of rows) { + const key = row.key as string; + let value = row.value as string | null; + + if (key === 'github_token' && value) { + try { + const decrypted = decrypt(value, config.encryptionKey); + value = maskApiKey(decrypted); + } catch { + value = '****'; + } + } + + settings[key] = value; + } + + res.json(settings); + } catch (err) { + console.error('GET /api/settings error:', err); + res.status(500).json({ error: 'Failed to fetch settings', code: 'FETCH_SETTINGS_FAILED' }); + } +}); + +// PUT /api/settings +router.put('/api/settings', (req, res) => { + try { + const db = getDb(); + const updates = req.body as Record; + + const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'); + + const upsert = db.transaction(() => { + for (const [key, rawValue] of Object.entries(updates)) { + let value = rawValue as string | null; + + if (key === 'github_token' && value && typeof value === 'string') { + if (value.startsWith('***')) { + // Skip masked values — keep existing + continue; + } + value = encrypt(value, config.encryptionKey); + } + + stmt.run(key, value ?? null); + } + }); + + upsert(); + res.json({ updated: true }); + } catch (err) { + console.error('PUT /api/settings error:', err); + res.status(500).json({ error: 'Failed to update settings', code: 'UPDATE_SETTINGS_FAILED' }); + } +}); + +export default router; diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts new file mode 100644 index 0000000..8f5f8d5 --- /dev/null +++ b/server/src/routes/health.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; + +const router = Router(); + +router.get('/api/health', (_req, res) => { + res.json({ + status: 'ok', + version: '0.1.0', + timestamp: new Date().toISOString(), + }); +}); + +export default router; diff --git a/server/src/routes/proxy.ts b/server/src/routes/proxy.ts new file mode 100644 index 0000000..b3d864c --- /dev/null +++ b/server/src/routes/proxy.ts @@ -0,0 +1,191 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; +import { decrypt } from '../services/crypto.js'; +import { config } from '../config.js'; +import { proxyRequest } from '../services/proxyService.js'; + +const router = Router(); + +// Helper: build API URL handling baseUrl already ending in version prefix +function buildApiUrl(baseUrl: string, pathWithVersion: string): string { + const baseUrlWithSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + const versionPrefix = pathWithVersion.split('/')[0] || ''; + + try { + const base = new URL(baseUrlWithSlash); + const basePath = base.pathname.replace(/\/$/, ''); + + if (versionPrefix) { + const versionRe = new RegExp(`/${versionPrefix}$`); + if (versionRe.test(basePath) && pathWithVersion.startsWith(`${versionPrefix}/`)) { + const rest = pathWithVersion.slice(versionPrefix.length + 1); + return new URL(rest, baseUrlWithSlash).toString(); + } + } + + return new URL(pathWithVersion, baseUrlWithSlash).toString(); + } catch { + return `${baseUrlWithSlash}${pathWithVersion}`; + } +} + +// POST /api/proxy/github/* +router.post('/api/proxy/github/*', async (req, res) => { + try { + const db = getDb(); + const githubPath = (req.params as Record)[0]; // wildcard capture + + // Read and decrypt GitHub token from settings + const tokenRow = db.prepare('SELECT value FROM settings WHERE key = ?').get('github_token') as { value: string } | undefined; + if (!tokenRow?.value) { + res.status(400).json({ error: 'GitHub token not configured', code: 'GITHUB_TOKEN_NOT_CONFIGURED' }); + return; + } + + let token: string; + try { + token = decrypt(tokenRow.value, config.encryptionKey); + } catch { + res.status(500).json({ error: 'Failed to decrypt GitHub token', code: 'GITHUB_TOKEN_DECRYPT_FAILED' }); + return; + } + + // Build target URL with query params + const queryString = new URL(req.url, 'http://localhost').search; + const targetUrl = `https://api.github.com/${githubPath}${queryString}`; + + const body = req.body as { method?: string; headers?: Record }; + const method = body.method || 'GET'; + + const headers: Record = { + 'Authorization': `Bearer ${token}`, + 'Accept': body.headers?.Accept || 'application/vnd.github.v3+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'GithubStarsManager-Backend', + }; + + const result = await proxyRequest({ url: targetUrl, method, headers }); + res.status(result.status).json(result.data); + } catch (err) { + console.error('GitHub proxy error:', err); + res.status(500).json({ error: 'GitHub proxy failed', code: 'GITHUB_PROXY_FAILED' }); + } +}); + +// POST /api/proxy/ai +router.post('/api/proxy/ai', async (req, res) => { + try { + const db = getDb(); + const { configId, body: requestBody } = req.body as { configId: string; body: Record }; + + if (!configId) { + res.status(400).json({ error: 'configId required', code: 'CONFIG_ID_REQUIRED' }); + return; + } + + const aiConfig = db.prepare('SELECT * FROM ai_configs WHERE id = ?').get(configId) as Record | undefined; + if (!aiConfig) { + res.status(404).json({ error: 'AI config not found', code: 'AI_CONFIG_NOT_FOUND' }); + return; + } + + const apiKey = decrypt(aiConfig.api_key_encrypted as string, config.encryptionKey); + const apiType = (aiConfig.api_type as string) || 'openai'; + const baseUrl = aiConfig.base_url as string; + const model = aiConfig.model as string; + + let targetUrl: string; + const headers: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + if (apiType === 'openai') { + targetUrl = buildApiUrl(baseUrl, 'v1/chat/completions'); + headers['Authorization'] = `Bearer ${apiKey}`; + } else if (apiType === 'claude') { + targetUrl = buildApiUrl(baseUrl, 'v1/messages'); + headers['x-api-key'] = apiKey; + headers['anthropic-version'] = '2023-06-01'; + } else { + // gemini + const rawModel = model.trim(); + const modelName = rawModel.startsWith('models/') ? rawModel.slice('models/'.length) : rawModel; + const path = `v1beta/models/${encodeURIComponent(modelName)}:generateContent`; + targetUrl = buildApiUrl(baseUrl, path); + const urlObj = new URL(targetUrl); + urlObj.searchParams.set('key', apiKey); + targetUrl = urlObj.toString(); + } + + const result = await proxyRequest({ + url: targetUrl, + method: 'POST', + headers, + body: requestBody, + timeout: 60000, + }); + + res.status(result.status).json(result.data); + } catch (err) { + console.error('AI proxy error:', err); + res.status(500).json({ error: 'AI proxy failed', code: 'AI_PROXY_FAILED' }); + } +}); + +// POST /api/proxy/webdav +router.post('/api/proxy/webdav', async (req, res) => { + try { + const db = getDb(); + const { configId, method, path, body: requestBody, headers: extraHeaders } = req.body as { + configId: string; + method: string; + path: string; + body?: string; + headers?: Record; + }; + + if (!configId) { + res.status(400).json({ error: 'configId required', code: 'CONFIG_ID_REQUIRED' }); + return; + } + + const webdavConfig = db.prepare('SELECT * FROM webdav_configs WHERE id = ?').get(configId) as Record | undefined; + if (!webdavConfig) { + res.status(404).json({ error: 'WebDAV config not found', code: 'WEBDAV_CONFIG_NOT_FOUND' }); + return; + } + + const password = decrypt(webdavConfig.password_encrypted as string, config.encryptionKey); + const username = webdavConfig.username as string; + const baseUrl = webdavConfig.url as string; + + const targetUrl = `${baseUrl}${path}`; + const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + + const { Authorization: _ignored, ...safeHeaders } = extraHeaders || {}; + const headers: Record = { + ...safeHeaders, + 'Authorization': `Basic ${credentials}`, + }; + + if (method === 'PROPFIND') { + headers['Content-Type'] = headers['Content-Type'] || 'application/xml'; + } + + const result = await proxyRequest({ + url: targetUrl, + method, + headers, + body: requestBody, + timeout: 60000, + }); + + res.status(result.status).json(result.data); + } catch (err) { + console.error('WebDAV proxy error:', err); + res.status(500).json({ error: 'WebDAV proxy failed', code: 'WEBDAV_PROXY_FAILED' }); + } +}); + +export default router; diff --git a/server/src/routes/releases.ts b/server/src/routes/releases.ts new file mode 100644 index 0000000..2327399 --- /dev/null +++ b/server/src/routes/releases.ts @@ -0,0 +1,173 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; + +const router = Router(); + +function parseJsonColumn(value: unknown): unknown[] { + if (typeof value !== 'string' || !value) return []; + try { return JSON.parse(value); } catch { return []; } +} + +function transformRelease(row: Record) { + return { + id: row.id, + tag_name: row.tag_name, + name: row.name, + body: row.body, + html_url: row.html_url, + published_at: row.published_at, + prerelease: !!row.prerelease, + draft: !!row.draft, + is_read: !!row.is_read, + assets: parseJsonColumn(row.assets), + repository: { + id: row.repo_id, + full_name: row.repo_full_name, + name: row.repo_name, + }, + }; +} + +// GET /api/releases +router.get('/api/releases', (req, res) => { + try { + const db = getDb(); + const page = Math.max(1, parseInt(req.query.page as string) || 1); + const limit = Math.min(10000, Math.max(1, parseInt(req.query.limit as string) || 50)); + const repoId = req.query.repo_id as string | undefined; + const unread = req.query.unread as string | undefined; + const offset = (page - 1) * limit; + + let sql = 'SELECT * FROM releases'; + const conditions: string[] = []; + const params: unknown[] = []; + + if (repoId) { + conditions.push('repo_id = ?'); + params.push(parseInt(repoId)); + } + if (unread === 'true') { + conditions.push('is_read = 0'); + } + + if (conditions.length > 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + + sql += ' ORDER BY published_at DESC LIMIT ? OFFSET ?'; + params.push(limit, offset); + + const rows = db.prepare(sql).all(...params) as Record[]; + const releases = rows.map(transformRelease); + + let countSql = 'SELECT COUNT(*) as total FROM releases'; + const countParams: unknown[] = []; + if (conditions.length > 0) { + countSql += ' WHERE ' + conditions.join(' AND '); + if (repoId) countParams.push(parseInt(repoId)); + } + const countRow = db.prepare(countSql).get(...countParams) as { total: number }; + + res.json({ releases, total: countRow.total, page, limit }); + } catch (err) { + console.error('GET /api/releases error:', err); + res.status(500).json({ error: 'Failed to fetch releases', code: 'FETCH_RELEASES_FAILED' }); + } +}); + +// PUT /api/releases (bulk upsert) +router.put('/api/releases', (req, res) => { + try { + const db = getDb(); + const { releases } = req.body as { releases: Record[] }; + if (!Array.isArray(releases)) { + res.status(400).json({ error: 'releases array required', code: 'RELEASES_ARRAY_REQUIRED' }); + return; + } + + for (const release of releases) { + if (!release.id) { + res.status(400).json({ error: 'Each release must have an id', code: 'RELEASE_ID_REQUIRED' }); + return; + } + } + + const stmt = db.prepare(` + INSERT OR REPLACE INTO releases ( + id, tag_name, name, body, html_url, published_at, + prerelease, draft, is_read, assets, + repo_id, repo_full_name, repo_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + const upsert = db.transaction(() => { + let count = 0; + for (const release of releases) { + const repository = release.repository as { id?: number; full_name?: string; name?: string } | undefined; + stmt.run( + release.id, + release.tag_name ?? null, + release.name ?? null, + release.body ?? null, + release.html_url ?? null, + release.published_at ?? null, + release.prerelease ? 1 : 0, + release.draft ? 1 : 0, + release.is_read ? 1 : 0, + JSON.stringify(release.assets ?? []), + repository?.id ?? release.repo_id ?? null, + repository?.full_name ?? release.repo_full_name ?? null, + repository?.name ?? release.repo_name ?? null + ); + count++; + } + return count; + }); + + const count = upsert(); + res.json({ upserted: count }); + } catch (err) { + console.error('PUT /api/releases error:', err); + res.status(500).json({ error: 'Failed to upsert releases', code: 'UPSERT_RELEASES_FAILED' }); + } +}); + +// PATCH /api/releases/:id +router.patch('/api/releases/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const { is_read } = req.body as { is_read?: boolean }; + + if (is_read === undefined) { + res.status(400).json({ error: 'is_read field required', code: 'IS_READ_REQUIRED' }); + return; + } + + db.prepare('UPDATE releases SET is_read = ? WHERE id = ?').run(is_read ? 1 : 0, id); + + const row = db.prepare('SELECT * FROM releases WHERE id = ?').get(id) as Record | undefined; + if (!row) { + res.status(404).json({ error: 'Release not found', code: 'RELEASE_NOT_FOUND' }); + return; + } + res.json(transformRelease(row)); + } catch (err) { + console.error('PATCH /api/releases error:', err); + res.status(500).json({ error: 'Failed to update release', code: 'UPDATE_RELEASE_FAILED' }); + } +}); + +// POST /api/releases/mark-all-read +router.post('/api/releases/mark-all-read', (_req, res) => { + try { + const db = getDb(); + const result = db.prepare('UPDATE releases SET is_read = 1').run(); + res.json({ updated: result.changes }); + } catch (err) { + console.error('POST /api/releases/mark-all-read error:', err); + res.status(500).json({ error: 'Failed to mark all as read', code: 'MARK_ALL_READ_FAILED' }); + } +}); + +export default router; diff --git a/server/src/routes/repositories.ts b/server/src/routes/repositories.ts new file mode 100644 index 0000000..0963044 --- /dev/null +++ b/server/src/routes/repositories.ts @@ -0,0 +1,188 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; + +const router = Router(); + +// Helper to parse JSON columns safely +function parseJsonColumn(value: unknown): unknown[] { + if (typeof value !== 'string' || !value) return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { return []; } +} + +// Helper to transform DB row to API response +function transformRepo(row: Record) { + return { + id: row.id, + name: row.name, + full_name: row.full_name, + description: row.description, + html_url: row.html_url, + stargazers_count: row.stargazers_count, + language: row.language, + created_at: row.created_at, + updated_at: row.updated_at, + pushed_at: row.pushed_at, + starred_at: row.starred_at, + owner: { login: row.owner_login, avatar_url: row.owner_avatar_url }, + topics: parseJsonColumn(row.topics), + ai_summary: row.ai_summary, + ai_tags: parseJsonColumn(row.ai_tags), + ai_platforms: parseJsonColumn(row.ai_platforms), + analyzed_at: row.analyzed_at, + analysis_failed: !!row.analysis_failed, + custom_description: row.custom_description, + custom_tags: parseJsonColumn(row.custom_tags), + custom_category: row.custom_category, + last_edited: row.last_edited, + subscribed_to_releases: !!row.subscribed_to_releases, + }; +} + +// GET /api/repositories +router.get('/api/repositories', (req, res) => { + try { + const db = getDb(); + const page = Math.max(1, parseInt(req.query.page as string) || 1); + const limit = Math.min(10000, Math.max(1, parseInt(req.query.limit as string) || 100)); + const search = req.query.search as string | undefined; + const offset = (page - 1) * limit; + + let sql = 'SELECT * FROM repositories'; + const params: unknown[] = []; + + if (search) { + const escaped = search.replace(/[%_\\]/g, '\\$&'); + sql += " WHERE name LIKE ? ESCAPE '\\' OR full_name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR ai_summary LIKE ? ESCAPE '\\' OR ai_tags LIKE ? ESCAPE '\\'"; + const searchPattern = `%${escaped}%`; + params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern); + } + + sql += ' ORDER BY stargazers_count DESC LIMIT ? OFFSET ?'; + params.push(limit, offset); + + const rows = db.prepare(sql).all(...params) as Record[]; + const repositories = rows.map(transformRepo); + + const countSql = search + ? 'SELECT COUNT(*) as total FROM repositories WHERE name LIKE ? OR full_name LIKE ? OR description LIKE ? OR ai_summary LIKE ? OR ai_tags LIKE ?' + : 'SELECT COUNT(*) as total FROM repositories'; + const countParams = search ? Array(5).fill(`%${search}%`) : []; + const countRow = db.prepare(countSql).get(...countParams) as { total: number }; + + res.json({ repositories, total: countRow.total, page, limit }); + } catch (err) { + console.error('GET /api/repositories error:', err); + res.status(500).json({ error: 'Failed to fetch repositories', code: 'FETCH_REPOSITORIES_FAILED' }); + } +}); + +// PUT /api/repositories (bulk upsert) +router.put('/api/repositories', (req, res) => { + try { + const db = getDb(); + const { repositories } = req.body as { repositories: Record[] }; + if (!Array.isArray(repositories)) { + res.status(400).json({ error: 'repositories array required', code: 'REPOSITORIES_ARRAY_REQUIRED' }); + return; + } + + const stmt = db.prepare(` + INSERT OR REPLACE INTO repositories ( + id, name, full_name, description, html_url, stargazers_count, language, + created_at, updated_at, pushed_at, starred_at, + owner_login, owner_avatar_url, topics, + ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed, + custom_description, custom_tags, custom_category, last_edited, + subscribed_to_releases + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + const upsert = db.transaction(() => { + let count = 0; + for (const repo of repositories) { + const owner = repo.owner as { login?: string; avatar_url?: string } | undefined; + stmt.run( + repo.id, repo.name, repo.full_name, repo.description ?? null, + repo.html_url, repo.stargazers_count ?? 0, repo.language ?? null, + repo.created_at ?? null, repo.updated_at ?? null, repo.pushed_at ?? null, + repo.starred_at ?? null, + owner?.login ?? '', owner?.avatar_url ?? null, + JSON.stringify(Array.isArray(repo.topics) ? repo.topics : []), + repo.ai_summary ?? null, + JSON.stringify(Array.isArray(repo.ai_tags) ? repo.ai_tags : []), + JSON.stringify(Array.isArray(repo.ai_platforms) ? repo.ai_platforms : []), + repo.analyzed_at ?? null, (repo.analysis_failed === true || repo.analysis_failed === 1) ? 1 : 0, + repo.custom_description ?? null, + JSON.stringify(Array.isArray(repo.custom_tags) ? repo.custom_tags : []), + repo.custom_category ?? null, repo.last_edited ?? null, + (repo.subscribed_to_releases === true || repo.subscribed_to_releases === 1) ? 1 : 0 + ); + count++; + } + return count; + }); + + const count = upsert(); + res.json({ upserted: count }); + } catch (err) { + console.error('PUT /api/repositories error:', err); + res.status(500).json({ error: 'Failed to upsert repositories', code: 'UPSERT_REPOSITORIES_FAILED' }); + } +}); + +// PATCH /api/repositories/:id +router.patch('/api/repositories/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + const updates = req.body as Record; + + const allowedFields: Record unknown> = { + ai_summary: (v) => v, + ai_tags: (v) => JSON.stringify(Array.isArray(v) ? v : []), + ai_platforms: (v) => JSON.stringify(Array.isArray(v) ? v : []), + analyzed_at: (v) => v, + analysis_failed: (v) => (v === true || v === 1) ? 1 : 0, + custom_description: (v) => v, + custom_tags: (v) => JSON.stringify(Array.isArray(v) ? v : []), + custom_category: (v) => v, + last_edited: (v) => v, + subscribed_to_releases: (v) => (v === true || v === 1) ? 1 : 0, + description: (v) => v, + name: (v) => v, + }; + + const setClauses: string[] = []; + const values: unknown[] = []; + + for (const [key, transform] of Object.entries(allowedFields)) { + if (key in updates) { + setClauses.push(`${key} = ?`); + values.push(transform(updates[key])); + } + } + + if (setClauses.length === 0) { + res.status(400).json({ error: 'No valid fields to update', code: 'NO_VALID_FIELDS' }); + return; + } + + values.push(id); + db.prepare(`UPDATE repositories SET ${setClauses.join(', ')} WHERE id = ?`).run(...values); + + const row = db.prepare('SELECT * FROM repositories WHERE id = ?').get(id) as Record | undefined; + if (!row) { + res.status(404).json({ error: 'Repository not found', code: 'REPOSITORY_NOT_FOUND' }); + return; + } + res.json(transformRepo(row)); + } catch (err) { + console.error('PATCH /api/repositories error:', err); + res.status(500).json({ error: 'Failed to update repository', code: 'UPDATE_REPOSITORY_FAILED' }); + } +}); + +export default router; diff --git a/server/src/routes/sync.ts b/server/src/routes/sync.ts new file mode 100644 index 0000000..1784af8 --- /dev/null +++ b/server/src/routes/sync.ts @@ -0,0 +1,251 @@ +import { Router } from 'express'; +import { getDb } from '../db/connection.js'; +import { encrypt, decrypt } from '../services/crypto.js'; +import { config } from '../config.js'; + +const router = Router(); + +function maskApiKey(key: string | null | undefined): string { + if (!key || typeof key !== 'string') return ''; + if (key.length <= 4) return '****'; + return '***' + key.slice(-4); +} + +// POST /api/sync/export +router.post('/api/sync/export', (_req, res) => { + try { + const db = getDb(); + + const repositories = db.prepare('SELECT * FROM repositories').all() as Record[]; + const releases = db.prepare('SELECT * FROM releases').all() as Record[]; + const categories = db.prepare('SELECT * FROM categories').all() as Record[]; + const assetFilters = db.prepare('SELECT * FROM asset_filters').all() as Record[]; + + // AI configs — mask api_key + const aiConfigRows = db.prepare('SELECT * FROM ai_configs').all() as Record[]; + const aiConfigs = aiConfigRows.map((row) => { + const masked = { ...row }; + if (masked.api_key_encrypted && typeof masked.api_key_encrypted === 'string') { + try { + masked.api_key_masked = maskApiKey(decrypt(masked.api_key_encrypted, config.encryptionKey)); + } catch { + masked.api_key_masked = '****'; + } + } + delete masked.api_key_encrypted; + return masked; + }); + + // WebDAV configs — mask password + const webdavRows = db.prepare('SELECT * FROM webdav_configs').all() as Record[]; + const webdavConfigs = webdavRows.map((row) => { + const masked = { ...row }; + if (masked.password_encrypted && typeof masked.password_encrypted === 'string') { + try { + masked.password_masked = maskApiKey(decrypt(masked.password_encrypted, config.encryptionKey)); + } catch { + masked.password_masked = '****'; + } + } + delete masked.password_encrypted; + return masked; + }); + + // Settings — mask github_token + const settingsRows = db.prepare('SELECT * FROM settings').all() as Record[]; + const settings: Record = {}; + for (const row of settingsRows) { + const key = row.key as string; + let value = row.value as string | null; + if (key === 'github_token' && value) { + try { + value = maskApiKey(decrypt(value, config.encryptionKey)); + } catch { + value = '****'; + } + } + settings[key] = value; + } + + res.json({ + version: 1, + exported_at: new Date().toISOString(), + repositories, + releases, + categories, + asset_filters: assetFilters, + ai_configs: aiConfigs, + webdav_configs: webdavConfigs, + settings, + }); + } catch (err) { + console.error('POST /api/sync/export error:', err); + res.status(500).json({ error: 'Failed to export data', code: 'EXPORT_DATA_FAILED' }); + } +}); + +// POST /api/sync/import +router.post('/api/sync/import', (req, res) => { + try { + const db = getDb(); + const data = req.body as Record; + const counts: Record = {}; + + const importAll = db.transaction(() => { + // Repositories + const repos = data.repositories as Record[] | undefined; + if (Array.isArray(repos) && repos.length > 0) { + const repoStmt = db.prepare(` + INSERT OR REPLACE INTO repositories ( + id, name, full_name, description, html_url, stargazers_count, language, + created_at, updated_at, pushed_at, starred_at, + owner_login, owner_avatar_url, topics, + ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed, + custom_description, custom_tags, custom_category, last_edited, + subscribed_to_releases + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const r of repos) { + repoStmt.run( + r.id, r.name, r.full_name, r.description ?? null, + r.html_url, r.stargazers_count ?? 0, r.language ?? null, + r.created_at ?? null, r.updated_at ?? null, r.pushed_at ?? null, + r.starred_at ?? null, + r.owner_login ?? '', r.owner_avatar_url ?? null, + typeof r.topics === 'string' ? r.topics : JSON.stringify(r.topics ?? []), + r.ai_summary ?? null, + typeof r.ai_tags === 'string' ? r.ai_tags : JSON.stringify(r.ai_tags ?? []), + typeof r.ai_platforms === 'string' ? r.ai_platforms : JSON.stringify(r.ai_platforms ?? []), + r.analyzed_at ?? null, r.analysis_failed ? 1 : 0, + r.custom_description ?? null, + typeof r.custom_tags === 'string' ? r.custom_tags : JSON.stringify(r.custom_tags ?? []), + r.custom_category ?? null, r.last_edited ?? null, + r.subscribed_to_releases ? 1 : 0 + ); + } + counts.repositories = repos.length; + } + + // Releases + const rels = data.releases as Record[] | undefined; + if (Array.isArray(rels) && rels.length > 0) { + const relStmt = db.prepare(` + INSERT OR REPLACE INTO releases ( + id, tag_name, name, body, html_url, published_at, + prerelease, draft, is_read, assets, + repo_id, repo_full_name, repo_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const r of rels) { + relStmt.run( + r.id, r.tag_name ?? null, r.name ?? null, r.body ?? null, + r.html_url ?? null, r.published_at ?? null, + r.prerelease ? 1 : 0, r.draft ? 1 : 0, r.is_read ? 1 : 0, + typeof r.assets === 'string' ? r.assets : JSON.stringify(r.assets ?? []), + r.repo_id ?? null, r.repo_full_name ?? null, r.repo_name ?? null + ); + } + counts.releases = rels.length; + } + + // Categories + const cats = data.categories as Record[] | undefined; + if (Array.isArray(cats) && cats.length > 0) { + const catStmt = db.prepare(` + INSERT OR REPLACE INTO categories (id, name, description, icon, keywords, color, sort_order, is_custom) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const c of cats) { + catStmt.run( + c.id, c.name ?? '', c.description ?? null, c.icon ?? '📁', + typeof c.keywords === 'string' ? c.keywords : JSON.stringify(c.keywords ?? []), + c.color ?? null, c.sort_order ?? 0, c.is_custom ? 1 : 0 + ); + } + counts.categories = cats.length; + } + + // Asset Filters + const filters = data.asset_filters as Record[] | undefined; + if (Array.isArray(filters) && filters.length > 0) { + const filterStmt = db.prepare(` + INSERT OR REPLACE INTO asset_filters (id, name, description, keywords, platform, sort_order) + VALUES (?, ?, ?, ?, ?, ?) + `); + for (const f of filters) { + filterStmt.run( + f.id, f.name ?? '', f.description ?? null, + typeof f.keywords === 'string' ? f.keywords : JSON.stringify(f.keywords ?? []), + f.platform ?? null, f.sort_order ?? 0 + ); + } + counts.asset_filters = filters.length; + } + + // AI Configs — skip masked secrets + const aiConfigs = data.ai_configs as Record[] | undefined; + if (Array.isArray(aiConfigs) && aiConfigs.length > 0) { + for (const c of aiConfigs) { + const existing = db.prepare('SELECT api_key_encrypted FROM ai_configs WHERE id = ?').get(c.id) as Record | undefined; + const existingKey = (existing?.api_key_encrypted as string) ?? null; + // Skip masked keys, keep existing encrypted value + db.prepare(` + INSERT OR REPLACE INTO ai_configs (id, name, api_type, base_url, api_key_encrypted, model, is_active, custom_prompt, use_custom_prompt, concurrency) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + c.id, c.name ?? '', c.api_type ?? c.apiType ?? 'openai', c.base_url ?? c.baseUrl ?? null, + existingKey, c.model ?? '', + (c.is_active ?? c.isActive) ? 1 : 0, c.custom_prompt ?? c.customPrompt ?? null, + (c.use_custom_prompt ?? c.useCustomPrompt) ? 1 : 0, c.concurrency ?? 1 + ); + } + counts.ai_configs = aiConfigs.length; + } + + // WebDAV Configs — skip masked secrets + const webdavConfigs = data.webdav_configs as Record[] | undefined; + if (Array.isArray(webdavConfigs) && webdavConfigs.length > 0) { + for (const c of webdavConfigs) { + const existing = db.prepare('SELECT password_encrypted FROM webdav_configs WHERE id = ?').get(c.id) as Record | undefined; + const existingPwd = (existing?.password_encrypted as string) ?? null; + db.prepare(` + INSERT OR REPLACE INTO webdav_configs (id, name, url, username, password_encrypted, path, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).run( + c.id, c.name ?? '', c.url ?? '', c.username ?? '', + existingPwd, + c.path ?? '/', (c.is_active ?? c.isActive) ? 1 : 0 + ); + } + counts.webdav_configs = webdavConfigs.length; + } + + // Settings — skip masked github_token + const settings = data.settings as Record | undefined; + if (settings && typeof settings === 'object') { + const settingsStmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'); + let settingsCount = 0; + for (const [key, value] of Object.entries(settings)) { + if (key === 'github_token' && typeof value === 'string' && value.startsWith('***')) { + continue; // Skip masked token + } + if (key === 'github_token' && value && typeof value === 'string') { + settingsStmt.run(key, encrypt(value, config.encryptionKey)); + } else { + settingsStmt.run(key, (value as string) ?? null); + } + settingsCount++; + } + counts.settings = settingsCount; + } + }); + + importAll(); + res.json({ imported: counts }); + } catch (err) { + console.error('POST /api/sync/import error:', err); + res.status(500).json({ error: 'Failed to import data', code: 'IMPORT_DATA_FAILED' }); + } +}); + +export default router; diff --git a/server/src/services/crypto.ts b/server/src/services/crypto.ts new file mode 100644 index 0000000..630d119 --- /dev/null +++ b/server/src/services/crypto.ts @@ -0,0 +1,52 @@ +import crypto from 'node:crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 12; + +export function encrypt(plaintext: string, key: string): string { + const keyBuffer = Buffer.from(key, 'hex'); + if (keyBuffer.length !== 32) { + throw new Error(`Invalid encryption key: expected 32 bytes (64 hex chars), got ${keyBuffer.length} bytes`); + } + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv); + + const encrypted = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final(), + ]); + + const authTag = cipher.getAuthTag(); + + return [ + iv.toString('base64'), + encrypted.toString('base64'), + authTag.toString('base64'), + ].join(':'); +} + +export function decrypt(encryptedStr: string, key: string): string { + const parts = encryptedStr.split(':'); + if (parts.length !== 3) { + throw new Error('Invalid encrypted string format'); + } + + const [ivB64, ciphertextB64, authTagB64] = parts; + const keyBuffer = Buffer.from(key, 'hex'); + if (keyBuffer.length !== 32) { + throw new Error(`Invalid encryption key: expected 32 bytes (64 hex chars), got ${keyBuffer.length} bytes`); + } + const iv = Buffer.from(ivB64, 'base64'); + const ciphertext = Buffer.from(ciphertextB64, 'base64'); + const authTag = Buffer.from(authTagB64, 'base64'); + + const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv); + decipher.setAuthTag(authTag); + + const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + + return decrypted.toString('utf8'); +} diff --git a/server/src/services/proxyService.ts b/server/src/services/proxyService.ts new file mode 100644 index 0000000..5bcfe25 --- /dev/null +++ b/server/src/services/proxyService.ts @@ -0,0 +1,101 @@ +export interface ProxyRequestOptions { + url: string; + method: string; + headers?: Record; + body?: string | object; + timeout?: number; +} + +export interface ProxyResponse { + status: number; + headers: Record; + data: unknown; +} + +function redactUrl(rawUrl: string): string { + try { + const url = new URL(rawUrl); + for (const key of ['key', 'api_key', 'apikey', 'token', 'access_token', 'secret', 'client_secret', 'password', 'auth']) { + if (url.searchParams.has(key)) url.searchParams.set(key, '***'); + } + return url.toString(); + } catch { + return rawUrl; + } +} + +const BLOCKED_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', '169.254.169.254']); +const PRIVATE_IP_PATTERNS = [/^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./]; + +function validateUrl(rawUrl: string): void { + const parsed = new URL(rawUrl); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error(`Blocked proxy request: unsupported protocol '${parsed.protocol}'`); + } + const hostname = parsed.hostname; + if (BLOCKED_HOSTS.has(hostname)) { + throw new Error(`Blocked proxy request: hostname '${hostname}' is not allowed`); + } + if (PRIVATE_IP_PATTERNS.some(p => p.test(hostname))) { + throw new Error(`Blocked proxy request: private IP '${hostname}' is not allowed`); + } +} + +export async function proxyRequest(options: ProxyRequestOptions): Promise { + const { url, method, headers = {}, body, timeout = 30000 } = options; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + validateUrl(url); + console.log(`[Proxy] ${method} ${redactUrl(url)}`); + + const fetchOptions: RequestInit = { + method, + headers, + signal: controller.signal, + }; + + if (body && method !== 'GET' && method !== 'HEAD') { + fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body); + const hasContentType = Object.keys(headers).some( + k => k.toLowerCase() === 'content-type' + ); + if (!hasContentType) { + (fetchOptions.headers as Record)['Content-Type'] = 'application/json'; + } + } + + const response = await fetch(url, fetchOptions); + + console.log(`[Proxy] ${method} ${redactUrl(url)} -> ${response.status}`); + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + let data: unknown; + const contentType = response.headers.get('content-type') || ''; + const text = await response.text(); + if (contentType.includes('application/json') && text.length > 0) { + try { + data = JSON.parse(text); + } catch { + data = text; + } + } else { + data = text; + } + + return { status: response.status, headers: responseHeaders, data }; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + return { status: 504, headers: {}, data: { error: 'Gateway Timeout', code: 'GATEWAY_TIMEOUT' } }; + } + console.error(`[Proxy] Error: ${error instanceof Error ? error.message : 'Unknown error'}`); + return { status: 502, headers: {}, data: { error: 'Bad Gateway', code: 'BAD_GATEWAY', details: error instanceof Error ? error.message : 'Unknown error' } }; + } finally { + clearTimeout(timeoutId); + } +} diff --git a/server/tests/routes/proxy.test.ts b/server/tests/routes/proxy.test.ts new file mode 100644 index 0000000..4223233 --- /dev/null +++ b/server/tests/routes/proxy.test.ts @@ -0,0 +1,283 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { proxyRequest } from '../../src/services/proxyService.js'; + +// Store original fetch +const originalFetch = globalThis.fetch; + +describe('proxyRequest', () => { + let mockFetch: ReturnType; + + beforeEach(() => { + mockFetch = vi.fn(); + globalThis.fetch = mockFetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it('should forward GET request and return JSON response', async () => { + const responseData = { items: [1, 2, 3] }; + const headers = new Headers({ 'content-type': 'application/json', 'x-ratelimit': '100' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), + }); + + const result = await proxyRequest({ + url: 'https://api.github.com/user/starred', + method: 'GET', + headers: { 'Authorization': 'Bearer test-token' }, + }); + + expect(result.status).toBe(200); + expect(result.data).toEqual(responseData); + expect(result.headers['content-type']).toBe('application/json'); + expect(result.headers['x-ratelimit']).toBe('100'); + + expect(mockFetch).toHaveBeenCalledOnce(); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe('https://api.github.com/user/starred'); + expect(calledOptions.method).toBe('GET'); + expect(calledOptions.headers['Authorization']).toBe('Bearer test-token'); + expect(calledOptions.body).toBeUndefined(); + }); + + it('should forward POST request with JSON body', async () => { + const requestBody = { model: 'gpt-4', messages: [{ role: 'user', content: 'hello' }] }; + const responseData = { choices: [{ message: { content: 'hi' } }] }; + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), + }); + + const result = await proxyRequest({ + url: 'https://api.openai.com/v1/chat/completions', + method: 'POST', + headers: { 'Authorization': 'Bearer sk-test', 'Content-Type': 'application/json' }, + body: requestBody, + }); + + expect(result.status).toBe(200); + expect(result.data).toEqual(responseData); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe('POST'); + expect(calledOptions.body).toBe(JSON.stringify(requestBody)); + }); + + it('should forward POST request with string body', async () => { + const xmlBody = ''; + const headers = new Headers({ 'content-type': 'application/xml' }); + mockFetch.mockResolvedValueOnce({ + status: 207, + headers, + json: () => Promise.reject(new Error('not json')), + text: () => Promise.resolve(''), + }); + + const result = await proxyRequest({ + url: 'https://dav.example.com/remote.php/dav', + method: 'PROPFIND', + headers: { 'Content-Type': 'application/xml' }, + body: xmlBody, + }); + + expect(result.status).toBe(207); + expect(result.data).toBe(''); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBe(xmlBody); + }); + + it('should auto-set Content-Type to application/json when body is object and no Content-Type header', async () => { + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve({ ok: true }), + text: () => Promise.resolve('{"ok":true}'), + }); + + await proxyRequest({ + url: 'https://example.com/api', + method: 'POST', + body: { key: 'value' }, + }); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect((calledOptions.headers as Record)['Content-Type']).toBe('application/json'); + }); + + it('should NOT attach body for GET requests', async () => { + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve([]), + text: () => Promise.resolve('[]'), + }); + + await proxyRequest({ + url: 'https://api.github.com/repos', + method: 'GET', + body: { should: 'be-ignored' }, + }); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + + it('should NOT attach body for HEAD requests', async () => { + const headers = new Headers({}); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve(null), + text: () => Promise.resolve(''), + }); + + await proxyRequest({ + url: 'https://example.com/check', + method: 'HEAD', + body: 'ignored', + }); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + + it('should return text data when response is not JSON', async () => { + const headers = new Headers({ 'content-type': 'text/html' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.reject(new Error('not json')), + text: () => Promise.resolve('hello'), + }); + + const result = await proxyRequest({ + url: 'https://example.com/page', + method: 'GET', + }); + + expect(result.status).toBe(200); + expect(result.data).toBe('hello'); + }); + + it('should return 504 on timeout (AbortError)', async () => { + const abortError = new DOMException('The operation was aborted.', 'AbortError'); + mockFetch.mockRejectedValueOnce(abortError); + + const result = await proxyRequest({ + url: 'https://slow.example.com/api', + method: 'GET', + timeout: 100, + }); + + expect(result.status).toBe(504); + expect(result.data).toEqual({ error: 'Gateway Timeout', code: 'GATEWAY_TIMEOUT' }); + expect(result.headers).toEqual({}); + }); + + it('should return 502 on network error', async () => { + mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED')); + + const result = await proxyRequest({ + url: 'https://down.example.com/api', + method: 'GET', + }); + + expect(result.status).toBe(502); + expect(result.data).toEqual({ + error: 'Bad Gateway', + code: 'BAD_GATEWAY', + details: 'ECONNREFUSED', + }); + expect(result.headers).toEqual({}); + }); + + it('should pass abort signal to fetch', async () => { + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve({ ok: true }), + text: () => Promise.resolve('{"ok":true}'), + }); + + await proxyRequest({ + url: 'https://example.com/api', + method: 'GET', + timeout: 5000, + }); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeInstanceOf(AbortSignal); + }); + + it('should handle upstream 4xx/5xx status codes transparently', async () => { + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 403, + headers, + json: () => Promise.resolve({ message: 'Forbidden' }), + text: () => Promise.resolve('{"message":"Forbidden"}'), + }); + + const result = await proxyRequest({ + url: 'https://api.github.com/forbidden', + method: 'GET', + }); + + expect(result.status).toBe(403); + expect(result.data).toEqual({ message: 'Forbidden' }); + }); + + it('should use default timeout of 30000ms', async () => { + const headers = new Headers({ 'content-type': 'application/json' }); + mockFetch.mockResolvedValueOnce({ + status: 200, + headers, + json: () => Promise.resolve({}), + text: () => Promise.resolve('{}'), + }); + + // Just verify it doesn't throw — the default timeout is internal + const result = await proxyRequest({ + url: 'https://example.com/api', + method: 'GET', + }); + + expect(result.status).toBe(200); + }); + + it('should handle PUT method with body for WebDAV', async () => { + const headers = new Headers({ 'content-type': 'text/plain' }); + mockFetch.mockResolvedValueOnce({ + status: 201, + headers, + json: () => Promise.reject(new Error('not json')), + text: () => Promise.resolve(''), + }); + + const result = await proxyRequest({ + url: 'https://dav.example.com/remote.php/dav/backup.json', + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: '{"repos":[]}', + }); + + expect(result.status).toBe(201); + + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe('PUT'); + expect(calledOptions.body).toBe('{"repos":[]}'); + }); +}); diff --git a/server/tests/services/crypto.test.ts b/server/tests/services/crypto.test.ts new file mode 100644 index 0000000..9e51dc7 --- /dev/null +++ b/server/tests/services/crypto.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import crypto from 'node:crypto'; +import { encrypt, decrypt } from '../../src/services/crypto.js'; + +// Generate a test key (32-byte hex = 64 hex chars) +const testKey = crypto.randomBytes(32).toString('hex'); + +describe('Crypto Service', () => { + it('should encrypt and decrypt back to original', () => { + const plaintext = 'sk-my-super-secret-api-key-12345'; + const encrypted = encrypt(plaintext, testKey); + const decrypted = decrypt(encrypted, testKey); + expect(decrypted).toBe(plaintext); + }); + + it('should produce different ciphertexts for same plaintext (random IV)', () => { + const plaintext = 'same-plaintext'; + const encrypted1 = encrypt(plaintext, testKey); + const encrypted2 = encrypt(plaintext, testKey); + expect(encrypted1).not.toBe(encrypted2); + + // But both should decrypt to the same value + expect(decrypt(encrypted1, testKey)).toBe(plaintext); + expect(decrypt(encrypted2, testKey)).toBe(plaintext); + }); + + it('should throw error when decrypting with wrong key', () => { + const plaintext = 'secret-data'; + const encrypted = encrypt(plaintext, testKey); + const wrongKey = crypto.randomBytes(32).toString('hex'); + + expect(() => decrypt(encrypted, wrongKey)).toThrow(); + }); + + it('should throw error when ciphertext is tampered', () => { + const plaintext = 'important-secret'; + const encrypted = encrypt(plaintext, testKey); + + // Tamper with the ciphertext part + const parts = encrypted.split(':'); + const ciphertextBuf = Buffer.from(parts[1], 'base64'); + ciphertextBuf[0] = ciphertextBuf[0] ^ 0xff; // flip bits + parts[1] = ciphertextBuf.toString('base64'); + const tampered = parts.join(':'); + + expect(() => decrypt(tampered, testKey)).toThrow(); + }); + + it('should throw error for invalid encrypted string format', () => { + expect(() => decrypt('not-a-valid-format', testKey)).toThrow('Invalid encrypted string format'); + }); + + it('should handle empty string', () => { + const plaintext = ''; + const encrypted = encrypt(plaintext, testKey); + const decrypted = decrypt(encrypted, testKey); + expect(decrypted).toBe(plaintext); + }); + + it('should handle unicode content', () => { + const plaintext = '你好世界 🌍 héllo'; + const encrypted = encrypt(plaintext, testKey); + const decrypted = decrypt(encrypted, testKey); + expect(decrypted).toBe(plaintext); + }); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..391ca78 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/server/vitest.config.ts b/server/vitest.config.ts new file mode 100644 index 0000000..68f9768 --- /dev/null +++ b/server/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: './', + test: { + globals: false, + environment: 'node', + include: ['tests/**/*.test.ts'], + }, + css: false, +}); diff --git a/src/App.tsx b/src/App.tsx index c21c4af..2e9498f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,8 @@ import { SettingsPanel } from './components/SettingsPanel'; import { useAppStore } from './store/useAppStore'; import { useAutoUpdateCheck } from './components/UpdateChecker'; import { UpdateNotificationBanner } from './components/UpdateNotificationBanner'; +import { backend } from './services/backendAdapter'; +import { syncFromBackend, startAutoSync, stopAutoSync } from './services/autoSync'; function App() { const { @@ -33,6 +35,35 @@ function App() { } }, [theme]); + // Initialize backend adapter and auto-sync + useEffect(() => { + let unsubscribe: (() => void) | null = null; + let cancelled = false; + + const initBackend = async () => { + try { + await backend.init(); + if (backend.isAvailable && !cancelled) { + await syncFromBackend(); + if (!cancelled) { + unsubscribe = startAutoSync(); + } + } + } catch (err) { + console.error('Failed to initialize backend:', err); + } + }; + + initBackend(); + + return () => { + cancelled = true; + if (unsubscribe) { + stopAutoSync(unsubscribe); + } + }; + }, []); + // Show login screen if not authenticated if (!isAuthenticated) { return ; diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index f8605c2..77baeaf 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Bot, Plus, @@ -19,13 +19,15 @@ import { ExternalLink, Mail, Github, - Twitter + Twitter, + Server, } from 'lucide-react'; import { AIConfig, WebDAVConfig } from '../types'; import { useAppStore } from '../store/useAppStore'; import { AIService } from '../services/aiService'; import { WebDAVService } from '../services/webdavService'; import { UpdateChecker } from './UpdateChecker'; +import { backend } from '../services/backendAdapter'; export const SettingsPanel: React.FC = () => { const { @@ -53,6 +55,10 @@ export const SettingsPanel: React.FC = () => { setReleases, addCustomCategory, deleteCustomCategory, + backendApiSecret, + setBackendApiSecret, + setAIConfigs, + setWebDAVConfigs, } = useAppStore(); const [showAIForm, setShowAIForm] = useState(false); @@ -64,6 +70,27 @@ export const SettingsPanel: React.FC = () => { const [isBackingUp, setIsBackingUp] = useState(false); const [isRestoring, setIsRestoring] = useState(false); const [showCustomPrompt, setShowCustomPrompt] = useState(false); + const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('disconnected'); + const [backendHealth, setBackendHealth] = useState<{ version: string; timestamp: string } | null>(null); + const [isSyncingToBackend, setIsSyncingToBackend] = useState(false); + const [isSyncingFromBackend, setIsSyncingFromBackend] = useState(false); + const [backendSecretInput, setBackendSecretInput] = useState(backendApiSecret || ''); + + // Check backend status on mount + useEffect(() => { + const checkBackend = async () => { + setBackendStatus('checking'); + const health = await backend.checkHealth(); + if (health) { + setBackendStatus('connected'); + setBackendHealth({ version: health.version, timestamp: health.timestamp }); + } else { + setBackendStatus('disconnected'); + setBackendHealth(null); + } + }; + checkBackend(); + }, []); type AIFormState = { name: string; @@ -472,6 +499,91 @@ Focus on practicality and accurate categorization to help users quickly understa const t = (zh: string, en: string) => language === 'zh' ? zh : en; + const handleTestBackendConnection = async () => { + setBackendStatus('checking'); + // Save the secret first + setBackendApiSecret(backendSecretInput || null); + // Re-init and check + await backend.init(); + const health = await backend.checkHealth(); + if (health) { + setBackendStatus('connected'); + setBackendHealth({ version: health.version, timestamp: health.timestamp }); + alert(t('后端连接成功!', 'Backend connection successful!')); + } else { + setBackendStatus('disconnected'); + setBackendHealth(null); + alert(t('后端连接失败,请检查服务器是否运行。', 'Backend connection failed. Please check if the server is running.')); + } + }; + + const handleSyncToBackend = async () => { + if (!backend.isAvailable) { + alert(t('后端不可用', 'Backend not available')); + return; + } + setIsSyncingToBackend(true); + try { + await backend.syncRepositories(repositories); + await backend.syncReleases(releases); + await backend.syncAIConfigs(aiConfigs); + await backend.syncWebDAVConfigs(webdavConfigs); + alert(t( + `已同步到后端:仓库 ${repositories.length},发布 ${releases.length},AI配置 ${aiConfigs.length},WebDAV配置 ${webdavConfigs.length}`, + `Synced to backend: repos ${repositories.length}, releases ${releases.length}, AI configs ${aiConfigs.length}, WebDAV configs ${webdavConfigs.length}` + )); + } catch (error) { + console.error('Sync to backend failed:', error); + alert(`${t('同步失败', 'Sync failed')}: ${(error as Error).message}`); + } finally { + setIsSyncingToBackend(false); + } + }; + + const handleSyncFromBackend = async () => { + if (!backend.isAvailable) { + alert(t('后端不可用', 'Backend not available')); + return; + } + + if (!confirm(t( + '从后端同步将覆盖本地数据,是否继续?', + 'Syncing from backend will overwrite local data. Continue?' + ))) return; + + setIsSyncingFromBackend(true); + try { + const repoData = await backend.fetchRepositories(); + const releaseData = await backend.fetchReleases(); + const aiConfigData = await backend.fetchAIConfigs(); + const webdavConfigData = await backend.fetchWebDAVConfigs(); + + if (repoData.repositories.length > 0) { + setRepositories(repoData.repositories); + } + if (releaseData.releases.length > 0) { + setReleases(releaseData.releases); + } + if (aiConfigData.length > 0) { + setAIConfigs(aiConfigData); + } + if (webdavConfigData.length > 0) { + setWebDAVConfigs(webdavConfigData); + } + + alert(t( + `已从后端同步:仓库 ${repoData.repositories.length},发布 ${releaseData.releases.length},AI配置 ${aiConfigData.length},WebDAV配置 ${webdavConfigData.length}`, + `Synced from backend: repos ${repoData.repositories.length}, releases ${releaseData.releases.length}, AI configs ${aiConfigData.length}, WebDAV configs ${webdavConfigData.length}` + )); + } catch (error) { + console.error('Sync from backend failed:', error); + alert(`${t('同步失败', 'Sync failed')}: ${(error as Error).message}`); + } finally { + setIsSyncingFromBackend(false); + } + }; + + return (
{/* Update Check */} @@ -1066,6 +1178,100 @@ Focus on practicality and accurate categorization to help users quickly understa
)} + {/* Backend Server Configuration */} +
+
+
+ +

+ {t('后端服务器', 'Backend Server')} +

+ + {backendStatus === 'connected' ? '🟢 ' + t('已连接', 'Connected') + : backendStatus === 'checking' ? '🟡 ' + t('检查中...', 'Checking...') + : '🔴 ' + t('未连接', 'Not Connected')} + +
+
+ + {backendHealth && ( +
+

{t('版本', 'Version')}: {backendHealth.version}

+
+ )} + +
+
+ +
+ setBackendSecretInput(e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" + placeholder={t('输入后端 API_SECRET(可选)', 'Enter backend API_SECRET (optional)')} + /> + +
+

+ {t( + '如果后端设置了 API_SECRET 环境变量,在此输入相同的值。未设置则留空。', + 'If the backend has API_SECRET env var set, enter the same value here. Leave empty if not set.' + )} +

+
+ + {backend.isAvailable && ( +
+ + + +
+ )} +
+
+ ); }; diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 97bafd4..2d0f897 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -1,4 +1,5 @@ import { Repository, AIConfig } from '../types'; +import { backend } from './backendAdapter'; export class AIService { private config: AIConfig; @@ -50,34 +51,40 @@ export class AIService { const apiType = this.getApiType(); if (apiType === 'openai') { - const url = this.buildApiUrl('v1/chat/completions'); const messages = [ ...(options.system.trim() ? [{ role: 'system', content: options.system }] : []), { role: 'user', content: options.user }, ]; - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Bearer ${this.config.apiKey}`, - }, - body: JSON.stringify({ - model: this.config.model, - messages, - temperature: options.temperature, - max_tokens: options.maxTokens, - }), - signal: options.signal, - }); + const requestBody = { + model: this.config.model, + messages, + temperature: options.temperature, + max_tokens: options.maxTokens, + }; - if (!response.ok) { - throw new Error(`AI API error: ${response.status} ${response.statusText}`); + let data: any; + if (backend.isAvailable && this.config.id) { + data = await backend.proxyAIRequest(this.config.id, requestBody); + } else { + const url = this.buildApiUrl('v1/chat/completions'); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${this.config.apiKey}`, + }, + body: JSON.stringify(requestBody), + signal: options.signal, + }); + if (!response.ok) { + throw new Error(`AI API error: ${response.status} ${response.statusText}`); + } + data = await response.json(); } - const data = await response.json(); const content = data.choices?.[0]?.message?.content; if (!content) { throw new Error('No content received from AI service'); @@ -86,31 +93,36 @@ export class AIService { } if (apiType === 'claude') { - const url = this.buildApiUrl('v1/messages'); - const body = { + const requestBody = { model: this.config.model, ...(options.system.trim() ? { system: options.system } : {}), messages: [{ role: 'user', content: options.user }], temperature: options.temperature, max_tokens: options.maxTokens, }; - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'x-api-key': this.config.apiKey, - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify(body), - signal: options.signal, - }); - if (!response.ok) { - throw new Error(`AI API error: ${response.status} ${response.statusText}`); + let data: unknown; + if (backend.isAvailable && this.config.id) { + data = await backend.proxyAIRequest(this.config.id, requestBody); + } else { + const url = this.buildApiUrl('v1/messages'); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-api-key': this.config.apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify(requestBody), + signal: options.signal, + }); + if (!response.ok) { + throw new Error(`AI API error: ${response.status} ${response.statusText}`); + } + data = await response.json(); } - const data: unknown = await response.json(); const contentBlocks = (data as { content?: unknown }).content; if (Array.isArray(contentBlocks)) { const text = contentBlocks @@ -120,47 +132,52 @@ export class AIService { return block.type === 'text' && typeof block.text === 'string' ? block.text : ''; }) .join(''); - if (text) return text; } - throw new Error('No content received from AI service'); } // gemini const rawModel = this.config.model.trim(); const model = rawModel.startsWith('models/') ? rawModel.slice('models/'.length) : rawModel; - const path = `v1beta/models/${encodeURIComponent(model)}:generateContent`; - const urlObj = new URL(this.buildApiUrl(path)); - urlObj.searchParams.set('key', this.config.apiKey); - - const prompt = options.system ? `${options.system}\n\n${options.user}` : options.user; - const response = await fetch(urlObj.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify({ - contents: [ - { - role: 'user', - parts: [{ text: prompt }], - }, - ], - generationConfig: { - temperature: options.temperature, - maxOutputTokens: options.maxTokens, + const prompt = options.system ? `${options.system} + +${options.user}` : options.user; + const requestBody = { + contents: [ + { + role: 'user', + parts: [{ text: prompt }], }, - }), - signal: options.signal, - }); + ], + generationConfig: { + temperature: options.temperature, + maxOutputTokens: options.maxTokens, + }, + }; - if (!response.ok) { - throw new Error(`AI API error: ${response.status} ${response.statusText}`); + let data: unknown; + if (backend.isAvailable && this.config.id) { + data = await backend.proxyAIRequest(this.config.id, requestBody); + } else { + const path = `v1beta/models/${encodeURIComponent(model)}:generateContent`; + const urlObj = new URL(this.buildApiUrl(path)); + urlObj.searchParams.set('key', this.config.apiKey); + const response = await fetch(urlObj.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(requestBody), + signal: options.signal, + }); + if (!response.ok) { + throw new Error(`AI API error: ${response.status} ${response.statusText}`); + } + data = await response.json(); } - const data: unknown = await response.json(); const candidates = (data as { candidates?: unknown }).candidates; if (Array.isArray(candidates) && candidates.length > 0) { const parts = (candidates[0] as { content?: { parts?: unknown } }).content?.parts; @@ -175,7 +192,6 @@ export class AIService { if (text) return text; } } - throw new Error('No content received from AI service'); } diff --git a/src/services/autoSync.ts b/src/services/autoSync.ts new file mode 100644 index 0000000..c498f50 --- /dev/null +++ b/src/services/autoSync.ts @@ -0,0 +1,287 @@ +import { backend } from './backendAdapter'; +import { useAppStore } from '../store/useAppStore'; + +// Prevent sync loops: when we pull data FROM backend and update store, +// the store subscription would trigger a push TO backend. This flag blocks that. +let _isSyncingFromBackend = false; +let _isSyncingFromBackendActive = false; + +// Track store subscription for cleanup on restart +let _storeUnsubscribe: (() => void) | null = null; + +// Prevent overlapping pushes to backend +let _isPushingToBackend = false; +// Queue a push if one is requested while a pull is in-flight +let _hasPendingPush = false; + +// Debounce timer for push-to-backend +let _debounceTimer: ReturnType | null = null; + +// Polling timer for pull-from-backend +let _pollTimer: ReturnType | null = null; + +// Polling interval in milliseconds +const POLL_INTERVAL = 5000; + +// Last known backend data fingerprints — skip store update if unchanged +let _lastHash = { + repos: '', + releases: '', + ai: '', + webdav: '', + settings: '', +}; + +function quickHash(data: unknown): string { + return JSON.stringify(data); +} + +/** + * Pull all data from backend and update local store. + * Backend-first strategy: backend data overwrites local data. + * Silent: errors logged to console only. + */ +export async function syncFromBackend(): Promise { + if (!backend.isAvailable || _isSyncingFromBackendActive || _isPushingToBackend) return; + + _isSyncingFromBackendActive = true; + + try { + const [reposResult, releasesResult, aiResult, webdavResult, settingsResult] = await Promise.allSettled([ + backend.fetchRepositories(), + backend.fetchReleases(), + backend.fetchAIConfigs(), + backend.fetchWebDAVConfigs(), + backend.fetchSettings(), + ]); + + const changed = { repos: false, releases: false, ai: false, webdav: false, settings: false }; + + // Compute hashes for each slice — only mark changed if hash differs + const hashes: Record = {}; + if (reposResult.status === 'fulfilled') { + const hash = quickHash(reposResult.value.repositories); + if (hash !== _lastHash.repos) { + hashes.repos = hash; + changed.repos = true; + } + } + + if (releasesResult.status === 'fulfilled') { + const hash = quickHash(releasesResult.value.releases); + if (hash !== _lastHash.releases) { + hashes.releases = hash; + changed.releases = true; + } + } + + if (aiResult.status === 'fulfilled') { + const hash = quickHash(aiResult.value); + if (hash !== _lastHash.ai) { + hashes.ai = hash; + changed.ai = true; + } + } + + if (webdavResult.status === 'fulfilled') { + const hash = quickHash(webdavResult.value); + if (hash !== _lastHash.webdav) { + hashes.webdav = hash; + changed.webdav = true; + } + } + + if (settingsResult.status === 'fulfilled') { + const hash = quickHash(settingsResult.value); + if (hash !== _lastHash.settings) { + hashes.settings = hash; + changed.settings = true; + } + } + + // Only update store if backend data actually changed + if (!Object.values(changed).some(Boolean)) return; + + _isSyncingFromBackend = true; + const state = useAppStore.getState(); + + // Update store then commit hash — hash only changes if setter succeeds + if (changed.repos && reposResult.status === 'fulfilled') { + state.setRepositories(reposResult.value.repositories); + _lastHash.repos = hashes.repos; + } + if (changed.releases && releasesResult.status === 'fulfilled') { + state.setReleases(releasesResult.value.releases); + _lastHash.releases = hashes.releases; + } + if (changed.ai && aiResult.status === 'fulfilled') { + state.setAIConfigs(aiResult.value); + _lastHash.ai = hashes.ai; + } + if (changed.webdav && webdavResult.status === 'fulfilled') { + state.setWebDAVConfigs(webdavResult.value); + _lastHash.webdav = hashes.webdav; + } + // Sync active selections from settings + if (changed.settings && settingsResult.status === 'fulfilled') { + const settings = settingsResult.value; + if (typeof settings.activeAIConfig === 'string' || settings.activeAIConfig === null) { + state.setActiveAIConfig(settings.activeAIConfig as string | null); + } + if (typeof settings.activeWebDAVConfig === 'string' || settings.activeWebDAVConfig === null) { + state.setActiveWebDAVConfig(settings.activeWebDAVConfig as string | null); + } + _lastHash.settings = hashes.settings; + } + + console.log('✅ Synced from backend (data changed)'); + } catch (err) { + console.error('Failed to sync from backend:', err); + } finally { + _isSyncingFromBackend = false; + _isSyncingFromBackendActive = false; + // Drain pending push that was queued during pull + if (_hasPendingPush) { + _hasPendingPush = false; + void syncToBackend(); + } + } +} + +/** + * Push current local state to backend. + * Silent: errors logged to console only. + */ +export async function syncToBackend(): Promise { + if (!backend.isAvailable) return; + // If a pull is in-flight, queue this push for after pull completes + if (_isSyncingFromBackendActive) { + _hasPendingPush = true; + return; + } + if (_isSyncingFromBackend) return; + if (_isPushingToBackend) return; + + _isPushingToBackend = true; + _hasPendingPush = false; + try { + const state = useAppStore.getState(); + + const results = await Promise.allSettled([ + backend.syncRepositories(state.repositories), + backend.syncReleases(state.releases), + backend.syncAIConfigs(state.aiConfigs), + backend.syncWebDAVConfigs(state.webdavConfigs), + backend.syncSettings({ + activeAIConfig: state.activeAIConfig, + activeWebDAVConfig: state.activeWebDAVConfig, + }), + ]); + const [reposSync, releasesSync, aiSync, webdavSync, settingsSync] = results; + + const failures = results.filter(r => r.status === 'rejected'); + if (failures.length > 0) { + console.warn(`⚠️ Synced to backend with ${failures.length} error(s):`, failures.map(f => (f as PromiseRejectedResult).reason)); + } else { + console.log('✅ Synced to backend'); + } + + // Only update _lastHash for successfully synced slices + if (reposSync.status === 'fulfilled') _lastHash.repos = quickHash(state.repositories); + if (releasesSync.status === 'fulfilled') _lastHash.releases = quickHash(state.releases); + if (aiSync.status === 'fulfilled') _lastHash.ai = quickHash(state.aiConfigs); + if (webdavSync.status === 'fulfilled') _lastHash.webdav = quickHash(state.webdavConfigs); + if (settingsSync.status === 'fulfilled') { + _lastHash.settings = quickHash({ + activeAIConfig: state.activeAIConfig, + activeWebDAVConfig: state.activeWebDAVConfig, + }); + } + } catch (err) { + console.error('Failed to sync to backend:', err); + } finally { + _isPushingToBackend = false; + } +} + +/** + * Subscribe to Zustand store changes and auto-push to backend with 2s debounce. + * Returns an unsubscribe function for cleanup. + */ +export function startAutoSync(): () => void { + // Guard: if already running, stop previous instance first + if (_storeUnsubscribe) { + _storeUnsubscribe(); + _storeUnsubscribe = null; + } + if (_pollTimer) { + clearInterval(_pollTimer); + _pollTimer = null; + } + if (_debounceTimer) { + clearTimeout(_debounceTimer); + _debounceTimer = null; + } + // Reset in-flight state flags to prevent permanent sync blocking + _isSyncingFromBackend = false; + _isPushingToBackend = false; + _isSyncingFromBackendActive = false; + _hasPendingPush = false; + // 1. Subscribe to local changes → push to backend (2s debounce) + const unsubscribe = useAppStore.subscribe((state, prevState) => { + if (_isSyncingFromBackend) return; + + const changed = + state.repositories !== prevState.repositories || + state.releases !== prevState.releases || + state.aiConfigs !== prevState.aiConfigs || + state.webdavConfigs !== prevState.webdavConfigs || + state.activeAIConfig !== prevState.activeAIConfig || + state.activeWebDAVConfig !== prevState.activeWebDAVConfig; + + if (!changed) return; + + // Debounce: wait 2s after last change before pushing + if (_debounceTimer) { + clearTimeout(_debounceTimer); + } + _debounceTimer = setTimeout(() => { + syncToBackend(); + }, 2000); + }); + _storeUnsubscribe = unsubscribe; + + // 2. Poll backend every 5s → pull fresh data for cross-device sync + _pollTimer = setInterval(() => { + syncFromBackend(); + }, POLL_INTERVAL); + + console.log('🔄 Auto-sync started (push debounce: 2s, poll: 5s)'); + return unsubscribe; +} + +/** + * Stop auto-sync: clear debounce timer and unsubscribe from store. + */ +export function stopAutoSync(unsubscribe: () => void): void { + if (_debounceTimer) { + clearTimeout(_debounceTimer); + _debounceTimer = null; + } + if (_pollTimer) { + clearInterval(_pollTimer); + _pollTimer = null; + } + if (_storeUnsubscribe) { + _storeUnsubscribe(); + _storeUnsubscribe = null; + } else { + unsubscribe(); + } + // Reset in-flight state flags + _isPushingToBackend = false; + _isSyncingFromBackendActive = false; + _isSyncingFromBackend = false; + _hasPendingPush = false; + console.log('🔄 Auto-sync stopped'); +} diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts new file mode 100644 index 0000000..60a8aed --- /dev/null +++ b/src/services/backendAdapter.ts @@ -0,0 +1,354 @@ +import { translateBackendError } from '../utils/backendErrors'; + +import { Repository, Release, AIConfig, WebDAVConfig } from '../types'; + +class BackendAdapter { + private _backendUrl: string | null = null; + + async init(): Promise { + try { + // Try common backend URLs + const urls = [ + window.location.origin + '/api', + ]; + // Only probe localhost in development + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + urls.push('http://localhost:3000/api'); + } + + for (const baseUrl of urls) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + try { + const res = await fetch(`${baseUrl}/health`, { + signal: controller.signal, + }); + + if (res.ok) { + const data = await res.json(); + if (data.status === 'ok') { + this._backendUrl = baseUrl; + console.log(`✅ Backend connected: ${baseUrl}`); + return; + } + } + } catch { + // Try next URL + } finally { + clearTimeout(timeoutId); + } + } + + this._backendUrl = null; + console.log('ℹ️ Backend not available, using local-only mode'); + } catch { + this._backendUrl = null; + console.log('ℹ️ Backend not available, using local-only mode'); + } + } + + get isAvailable(): boolean { + return this._backendUrl !== null; + } + + get backendUrl(): string | null { + return this._backendUrl; + } + + private getAuthHeaders(): Record { + // Read from localStorage directly to avoid circular dependency with store + const storeData = localStorage.getItem('github-stars-manager'); + let secret = ''; + if (storeData) { + try { + const parsed = JSON.parse(storeData); + secret = parsed.state?.backendApiSecret || ''; + } catch { /* ignore */ } + } + + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (secret) { + headers['Authorization'] = `Bearer ${secret}`; + } + return headers; + } + private async fetchWithTimeout(url: string, options?: RequestInit, timeoutMs = 30000): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timeoutId); + } + } + private async throwTranslatedError(res: Response, fallbackPrefix: string): Promise { + let code: string | undefined; + try { + const data = await res.json(); + code = data.code; + } catch { /* body not JSON */ } + throw new Error(translateBackendError(code, `${fallbackPrefix}: ${res.status}`)); + } + + // === GitHub Proxy === + + async fetchStarredRepos(page = 1, perPage = 100): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/user/starred?page=${page}&per_page=${perPage}&sort=updated`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ + method: 'GET', + headers: { 'Accept': 'application/vnd.github.star+json' } + }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Backend proxy error'); + const data = await res.json(); + return (data as Record[]).map((item) => + (item as { starred_at?: string; repo?: Repository }).starred_at && (item as { repo?: Repository }).repo + ? { ...((item as { repo: Repository }).repo), starred_at: (item as { starred_at: string }).starred_at } + : item as unknown as Repository + ); + } + + async getCurrentUser(): Promise> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/user`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ method: 'GET' }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Backend proxy error'); + return res.json() as Promise>; + } + + async getRepositoryReadme(owner: string, repo: string): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + try { + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/repos/${owner}/${repo}/readme`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ method: 'GET' }) + }); + if (!res.ok) return ''; + const data = await res.json() as { encoding?: string; content?: string }; + if (data.encoding === 'base64' && data.content) { + const binaryStr = atob(data.content); + const bytes = Uint8Array.from(binaryStr, c => c.charCodeAt(0)); + return new TextDecoder().decode(bytes); + } + return data.content || ''; + } catch { + return ''; + } + } + + async getRepositoryReleases(owner: string, repo: string, page = 1, perPage = 30): Promise[]> { + if (!this._backendUrl) throw new Error('Backend not available'); + + try { + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/repos/${owner}/${repo}/releases?page=${page}&per_page=${perPage}`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ method: 'GET' }) + }); + if (!res.ok) return []; + return res.json() as Promise[]>; + } catch { + return []; + } + } + + async checkRateLimit(): Promise<{ remaining: number; reset: number }> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/rate_limit`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ method: 'GET' }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Backend proxy error'); + const data = await res.json() as { rate: { remaining: number; reset: number } }; + return { remaining: data.rate.remaining, reset: data.rate.reset }; + } + + // === AI Proxy === + + async proxyAIRequest(configId: string, body: object): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/ai`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configId, body }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'AI proxy error'); + return res.json(); + } + + // === WebDAV Proxy === + + async proxyWebDAV(configId: string, method: string, path: string, body?: string, headers?: Record): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + return this.fetchWithTimeout(`${this._backendUrl}/proxy/webdav`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configId, method, path, body, headers }) + }); + } + + // === Data Sync === + + async syncRepositories(repos: Repository[]): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithTimeout(`${this._backendUrl}/repositories`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify({ repositories: repos }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Sync repositories error'); + } + + async fetchRepositories(): Promise<{ repositories: Repository[]; total: number }> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/repositories?limit=10000`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch error'); + return res.json() as Promise<{ repositories: Repository[]; total: number }>; + } + + async syncReleases(releases: Release[]): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithTimeout(`${this._backendUrl}/releases`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify({ releases }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Sync releases error'); + } + + async fetchReleases(): Promise<{ releases: Release[]; total: number }> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/releases?limit=10000`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch error'); + return res.json() as Promise<{ releases: Release[]; total: number }>; + } + + async syncAIConfigs(configs: AIConfig[]): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/ai/bulk`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configs }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Sync AI configs error'); + } + + async fetchAIConfigs(): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/ai?decrypt=true`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch AI configs error'); + return res.json() as Promise; + } + + async syncWebDAVConfigs(configs: WebDAVConfig[]): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/webdav/bulk`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configs }) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Sync WebDAV configs error'); + } + + async fetchWebDAVConfigs(): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/webdav?decrypt=true`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch WebDAV configs error'); + return res.json() as Promise; + } + + + // === Settings (active selections) === + + async syncSettings(settings: Record): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithTimeout(`${this._backendUrl}/settings`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify(settings) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Sync settings error'); + } + + async fetchSettings(): Promise> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/settings`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch settings error'); + return res.json() as Promise>; + } + + async exportData(): Promise> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/sync/export`, { + method: 'POST', + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Export error'); + return res.json() as Promise>; + } + + async importData(data: Record): Promise> { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/sync/import`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify(data) + }); + if (!res.ok) await this.throwTranslatedError(res, 'Import error'); + return res.json() as Promise>; + } + + // === Health === + + async checkHealth(): Promise<{ status: string; version: string; timestamp: string } | null> { + if (!this._backendUrl) return null; + + try { + const res = await this.fetchWithTimeout(`${this._backendUrl}/health`, undefined, 5000); + if (res.ok) return res.json() as Promise<{ status: string; version: string; timestamp: string }>; + return null; + } catch { + return null; + } + } +} + +export const backend = new BackendAdapter(); diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 7daeae1..ded304a 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -19,12 +19,14 @@ interface AppActions { updateAIConfig: (id: string, updates: Partial) => void; deleteAIConfig: (id: string) => void; setActiveAIConfig: (id: string | null) => void; + setAIConfigs: (configs: AIConfig[]) => void; // WebDAV actions addWebDAVConfig: (config: WebDAVConfig) => void; updateWebDAVConfig: (id: string, updates: Partial) => void; deleteWebDAVConfig: (id: string) => void; setActiveWebDAVConfig: (id: string | null) => void; + setWebDAVConfigs: (configs: WebDAVConfig[]) => void; setLastBackup: (timestamp: string) => void; // Search actions @@ -59,6 +61,9 @@ interface AppActions { // Update Analysis Progress setAnalysisProgress: (newProgress: AnalysisProgress) => void; + + // Backend actions + setBackendApiSecret: (secret: string | null) => void; } const initialSearchFilters: SearchFilters = { @@ -186,6 +191,7 @@ export const useAppStore = create()( language: 'zh', updateNotification: null, analysisProgress: { current: 0, total: 0 }, + backendApiSecret: null, // Auth actions setUser: (user) => { @@ -234,6 +240,7 @@ export const useAppStore = create()( activeAIConfig: state.activeAIConfig === id ? null : state.activeAIConfig })), setActiveAIConfig: (activeAIConfig) => set({ activeAIConfig }), + setAIConfigs: (aiConfigs) => set({ aiConfigs }), // WebDAV actions addWebDAVConfig: (config) => set((state) => ({ @@ -249,6 +256,7 @@ export const useAppStore = create()( activeWebDAVConfig: state.activeWebDAVConfig === id ? null : state.activeWebDAVConfig })), setActiveWebDAVConfig: (activeWebDAVConfig) => set({ activeWebDAVConfig }), + setWebDAVConfigs: (webdavConfigs) => set({ webdavConfigs }), setLastBackup: (lastBackup) => set({ lastBackup }), // Search actions @@ -320,7 +328,8 @@ export const useAppStore = create()( // Update actions setUpdateNotification: (notification) => set({ updateNotification: notification }), dismissUpdateNotification: () => set({ updateNotification: null }), - setAnalysisProgress: (newProgress) => set({ analysisProgress: newProgress }) + setAnalysisProgress: (newProgress) => set({ analysisProgress: newProgress }), + setBackendApiSecret: (backendApiSecret) => set({ backendApiSecret }), }), { name: 'github-stars-manager', @@ -357,6 +366,8 @@ export const useAppStore = create()( // 持久化UI设置 theme: state.theme, language: state.language, + + // backendApiSecret: 保留在内存中,不持久化(安全考虑) // 持久化搜索排序设置 searchFilters: { diff --git a/src/types/index.ts b/src/types/index.ts index 02677cd..cb5815d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -161,6 +161,9 @@ export interface AppState { // Analysis Progress analysisProgress: AnalysisProgress + + // Backend + backendApiSecret: string | null; } export interface UpdateNotification { diff --git a/src/utils/backendErrors.ts b/src/utils/backendErrors.ts new file mode 100644 index 0000000..00dbe5b --- /dev/null +++ b/src/utils/backendErrors.ts @@ -0,0 +1,81 @@ +const ERROR_MESSAGES: Record = { + // Auth + UNAUTHORIZED: { zh: '未授权,请检查 API Secret', en: 'Unauthorized, please check API Secret' }, + // Proxy + GITHUB_TOKEN_NOT_CONFIGURED: { zh: 'GitHub Token 未配置', en: 'GitHub token not configured' }, + GITHUB_TOKEN_DECRYPT_FAILED: { zh: 'GitHub Token 解密失败', en: 'Failed to decrypt GitHub token' }, + GITHUB_PROXY_FAILED: { zh: 'GitHub 代理请求失败', en: 'GitHub proxy failed' }, + CONFIG_ID_REQUIRED: { zh: '缺少配置 ID', en: 'Config ID required' }, + AI_CONFIG_NOT_FOUND: { zh: 'AI 配置未找到', en: 'AI config not found' }, + AI_PROXY_FAILED: { zh: 'AI 代理请求失败', en: 'AI proxy failed' }, + WEBDAV_CONFIG_NOT_FOUND: { zh: 'WebDAV 配置未找到', en: 'WebDAV config not found' }, + WEBDAV_PROXY_FAILED: { zh: 'WebDAV 代理请求失败', en: 'WebDAV proxy failed' }, + GATEWAY_TIMEOUT: { zh: '网关超时', en: 'Gateway Timeout' }, + BAD_GATEWAY: { zh: '网关错误', en: 'Bad Gateway' }, + // Repositories + FETCH_REPOSITORIES_FAILED: { zh: '获取仓库列表失败', en: 'Failed to fetch repositories' }, + REPOSITORIES_ARRAY_REQUIRED: { zh: '需要仓库数组', en: 'Repositories array required' }, + UPSERT_REPOSITORIES_FAILED: { zh: '更新仓库失败', en: 'Failed to upsert repositories' }, + NO_VALID_FIELDS: { zh: '没有有效的更新字段', en: 'No valid fields to update' }, + REPOSITORY_NOT_FOUND: { zh: '仓库未找到', en: 'Repository not found' }, + UPDATE_REPOSITORY_FAILED: { zh: '更新仓库失败', en: 'Failed to update repository' }, + // Releases + FETCH_RELEASES_FAILED: { zh: '获取发布列表失败', en: 'Failed to fetch releases' }, + RELEASES_ARRAY_REQUIRED: { zh: '需要发布数组', en: 'Releases array required' }, + UPSERT_RELEASES_FAILED: { zh: '更新发布信息失败', en: 'Failed to upsert releases' }, + IS_READ_REQUIRED: { zh: '需要 is_read 字段', en: 'is_read field required' }, + RELEASE_NOT_FOUND: { zh: '发布未找到', en: 'Release not found' }, + UPDATE_RELEASE_FAILED: { zh: '更新发布失败', en: 'Failed to update release' }, + MARK_ALL_READ_FAILED: { zh: '标记全部已读失败', en: 'Failed to mark all as read' }, + // Categories + FETCH_CATEGORIES_FAILED: { zh: '获取分类失败', en: 'Failed to fetch categories' }, + CREATE_CATEGORY_FAILED: { zh: '创建分类失败', en: 'Failed to create category' }, + CATEGORY_NOT_FOUND: { zh: '分类未找到', en: 'Category not found' }, + UPDATE_CATEGORY_FAILED: { zh: '更新分类失败', en: 'Failed to update category' }, + DELETE_CATEGORY_FAILED: { zh: '删除分类失败', en: 'Failed to delete category' }, + // Asset filters + FETCH_ASSET_FILTERS_FAILED: { zh: '获取资源过滤器失败', en: 'Failed to fetch asset filters' }, + CREATE_ASSET_FILTER_FAILED: { zh: '创建资源过滤器失败', en: 'Failed to create asset filter' }, + ASSET_FILTER_NOT_FOUND: { zh: '资源过滤器未找到', en: 'Asset filter not found' }, + UPDATE_ASSET_FILTER_FAILED: { zh: '更新资源过滤器失败', en: 'Failed to update asset filter' }, + DELETE_ASSET_FILTER_FAILED: { zh: '删除资源过滤器失败', en: 'Failed to delete asset filter' }, + // Configs + FETCH_AI_CONFIGS_FAILED: { zh: '获取 AI 配置失败', en: 'Failed to fetch AI configs' }, + CREATE_AI_CONFIG_FAILED: { zh: '创建 AI 配置失败', en: 'Failed to create AI config' }, + UPDATE_AI_CONFIG_FAILED: { zh: '更新 AI 配置失败', en: 'Failed to update AI config' }, + DELETE_AI_CONFIG_FAILED: { zh: '删除 AI 配置失败', en: 'Failed to delete AI config' }, + FETCH_WEBDAV_CONFIGS_FAILED: { zh: '获取 WebDAV 配置失败', en: 'Failed to fetch WebDAV configs' }, + CREATE_WEBDAV_CONFIG_FAILED: { zh: '创建 WebDAV 配置失败', en: 'Failed to create WebDAV config' }, + UPDATE_WEBDAV_CONFIG_FAILED: { zh: '更新 WebDAV 配置失败', en: 'Failed to update WebDAV config' }, + DELETE_WEBDAV_CONFIG_FAILED: { zh: '删除 WebDAV 配置失败', en: 'Failed to delete WebDAV config' }, + SYNC_AI_CONFIGS_FAILED: { zh: '同步 AI 配置失败', en: 'Failed to sync AI configs' }, + SYNC_WEBDAV_CONFIGS_FAILED: { zh: '同步 WebDAV 配置失败', en: 'Failed to sync WebDAV configs' }, + INVALID_REQUEST: { zh: '无效的请求', en: 'Invalid request' }, + FETCH_SETTINGS_FAILED: { zh: '获取设置失败', en: 'Failed to fetch settings' }, + UPDATE_SETTINGS_FAILED: { zh: '更新设置失败', en: 'Failed to update settings' }, + // Sync + EXPORT_DATA_FAILED: { zh: '导出数据失败', en: 'Failed to export data' }, + IMPORT_DATA_FAILED: { zh: '导入数据失败', en: 'Failed to import data' }, + // General + INTERNAL_SERVER_ERROR: { zh: '服务器内部错误', en: 'Internal server error' }, +}; + +function getCurrentLanguage(): 'zh' | 'en' { + try { + const storeData = localStorage.getItem('github-stars-manager'); + if (storeData) { + const parsed = JSON.parse(storeData); + const lang = parsed.state?.language; + if (lang === 'en') return 'en'; + } + } catch { /* ignore */ } + return 'zh'; +} + +export function translateBackendError(code: string | undefined, fallback: string): string { + if (!code) return fallback; + const entry = ERROR_MESSAGES[code]; + if (!entry) return fallback; + const lang = getCurrentLanguage(); + return entry[lang]; +}