commit d499020d4eb4a13fceb10826e03ad751545f0806 Author: FengLee Date: Sat May 9 11:32:34 2026 +0800 Initial miaojingAI project with image resolution guard diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..870f4b3 --- /dev/null +++ b/.babelrc @@ -0,0 +1,15 @@ +{ + "presets": [ + [ + "next/babel", + { + "preset-react": { + "development": true + } + } + ] + ], + "plugins": [ + "@react-dev-inspector/babel-plugin" + ] +} diff --git a/.coze b/.coze new file mode 100644 index 0000000..e4d6619 --- /dev/null +++ b/.coze @@ -0,0 +1,15 @@ +[project] +requires = [ "nodejs-24" ] +template = "nextjs" +version = "0.0.18" +appliedPatches = [ ] + +[dev] +build = [ "bash", "./scripts/prepare.sh" ] +run = [ "bash", "./scripts/dev.sh" ] +deps = [ "git" ] + +[deploy] +build = [ "bash", "./scripts/build.sh" ] +run = [ "bash", "./scripts/start.sh" ] +deps = [ "git" ] diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..758c488 --- /dev/null +++ b/.env.example @@ -0,0 +1,107 @@ +# ============================================================ +# 妙境 AI 创作平台 — 环境变量配置模板 +# 复制此文件为 .env.local 并填写实际值 +# cp .env.example .env.local +# ============================================================ + +# ----- 本地部署配置 (推荐) ----- +# 本地 PostgreSQL 数据库 +LOCAL_DB_URL=postgresql://postgres:postgres@localhost:5432/miaojing +LOCAL_DB_ANON_KEY=local-anon-key +LOCAL_DB_SERVICE_ROLE_KEY=local-service-role-key + +# ----- Supabase 云端配置 (可选) ----- +# 从 Supabase Dashboard → Settings → API 获取 +# 支持 COZE_ 前缀和不带前缀两种变量名 +# COZE_SUPABASE_URL=https://your-project.supabase.co +# COZE_SUPABASE_ANON_KEY=your-anon-key-here +# COZE_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here + +# 也可以使用不带 COZE_ 前缀的变量名 (二选一) +# SUPABASE_URL=https://your-project.supabase.co +# SUPABASE_ANON_KEY=your-anon-key-here +# SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here + +# ----- 服务端口 (可选) ----- +# 默认 5000,一般无需修改 +# DEPLOY_RUN_PORT=5000 +# MIAOJING_API_PORT=5100 +# MIAOJING_CONSOLE_PORT=5200 + +# ----- 管理员注册邀请码 (可选) ----- +# 注册时输入此邀请码可创建管理员账号 +# 默认值: miaojing-admin-2024 +# ADMIN_INVITE_CODE=miaojing-admin-2024 +# ADMIN_DEFAULT_PASSWORD=change-this-before-production + +# ----- 运行环境 (可选) ----- +# DEV = 开发环境, PROD = 生产环境 +# COZE_PROJECT_ENV=PROD +# NODE_ENV=production +# APP_BIND_HOST=127.0.0.1 +# 部署脚本默认自动安装/切换 Node.js 24 LTS;如需使用 22 LTS 可设置为 22 +# DEPLOY_NODE_MAJOR=24 +# DEPLOY_NODE_INSTALL_DIR=/var/lib/miaojingAI/node + +# ----- 项目域名 (可选) ----- +# 用于构造回调 URL、分享链接等 +# COZE_PROJECT_DOMAIN_DEFAULT=https://your-domain.com +# NEXT_PUBLIC_APP_URL=https://your-domain.com +# APP_BASE_URL=https://your-domain.com + +# ----- 生产安全密钥 (生产环境必须设置) ----- +# 建议使用 openssl rand -hex 32 生成 +# DATA_ENCRYPTION_KEY= +# JWT_SECRET= +# GENERATION_INTERNAL_SECRET= + +# ----- 持久化路径 (生产环境推荐放到项目目录外) ----- +# LOCAL_STORAGE_DIR=/var/lib/miaojingAI/storage +# BACKUP_DIR=/var/lib/miaojingAI/backups + +# ----- 数据库连接池 (可选) ----- +# DB_POOL_MAX=20 +# DB_CONNECTION_TIMEOUT_MS=5000 +# DB_IDLE_TIMEOUT_MS=30000 + +# ----- Node HTTP 服务超时 (可选) ----- +# HTTP_REQUEST_TIMEOUT_MS=190000 +# HTTP_HEADERS_TIMEOUT_MS=65000 +# HTTP_KEEP_ALIVE_TIMEOUT_MS=5000 +# HTTP_MAX_HEADERS_COUNT=200 + +# ----- 危险管理功能开关 ----- +# 生产环境保持 false。只有完成备份并明确需要清空非管理员用户时,才临时改为 true。 +# ENABLE_DANGER_ADMIN_CLEAR_USERS=false + +# ----- 应用层限流 (可选) ----- +# RATE_LIMIT_AUTH_MAX=10 +# RATE_LIMIT_EMAIL_MAX=6 +# RATE_LIMIT_GENERATION_MAX=20 +# RATE_LIMIT_DOWNLOAD_MAX=60 +# RATE_LIMIT_ADMIN_MAX=120 + +# ============================================================ +# 说明: +# - 本地部署模式: +# 1. 安装并启动本地 PostgreSQL 数据库 +# 2. 创建名为 miaojing 的数据库 +# 3. 运行 scripts/init-database.sql 初始化数据库结构 +# 4. 配置 LOCAL_DB_URL 等本地数据库环境变量 +# 5. 系统会自动使用本地存储替代 S3 存储 +# +# - Supabase 云端模式: +# 1. 创建 Supabase 项目 +# 2. 运行 scripts/init-database.sql 初始化数据库 +# 3. 在 Supabase Dashboard 创建 site-assets Storage 桶 (公开读) +# 4. 配置 COZE_SUPABASE_URL 等环境变量 +# +# - 无数据库配置时,系统将运行在 Demo 模式: +# Demo 模式下:登录/注册返回模拟数据,公告/网站配置返回默认值 +# 管理后台写入操作将返回 503 错误 +# +# - AI 图片/视频生成: +# 1. 内置使用 coze-coding-dev-sdk (开发环境可用) +# 2. 用户可在前端"自定义 API"中配置自己的 AI 模型密钥 +# 3. 管理员可在管理后台配置系统默认 API +# ============================================================ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..320b208 --- /dev/null +++ b/.gitignore @@ -0,0 +1,101 @@ +.next + +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Production build +dist/ +build/ +out/ +.next/ +.rsbuild/ + +# Testing +coverage/ +*.lcov +.nyc_output + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Cache +.cache/ +.parcel-cache/ +.eslintcache +.stylelintcache +.npm +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +node-compile-cache/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Temporary files +*.tmp +*.temp +_tmp_* +.tmp/ +.temp/ + +# Optional files +*.tgz +*.gz +*.zip +*.tar + +# TypeScript +*.tsbuildinfo + +# Misc +.vercel +.turbo + +.coze-logs diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..48c18ad --- /dev/null +++ b/.npmrc @@ -0,0 +1,26 @@ +loglevel=error +registry=https://registry.npmmirror.com + +strictStorePkgContentCheck=false +verifyStoreIntegrity=false + +# 网络优化 +network-concurrency=16 +fetch-retries=3 +fetch-timeout=60000 + +# 严格使用 peer dependencies +strict-peer-dependencies=false + +# 自动生成 lockfile +auto-install-peers=true + +# lockfile 配置 +lockfile=true +prefer-frozen-lockfile=true + +# 如果 lockfile 存在但过期,更新而不是失败 +resolution-mode=highest + +# 不提示 npm 更新 +update-notifier=false diff --git a/DEPLOY_BACKUP_UPGRADE.md b/DEPLOY_BACKUP_UPGRADE.md new file mode 100644 index 0000000..58fb2ff --- /dev/null +++ b/DEPLOY_BACKUP_UPGRADE.md @@ -0,0 +1,703 @@ +# 妙境 AI 创作平台部署、备份、升级操作文档 + +本文档用于生产环境的一键部署、日常备份、安全升级和故障回滚。所有命令默认在服务器源码目录执行。 + +## 1. 适用范围 + +适用于 Linux 服务器本地化部署架构: + +- 前端访问服务:`miaojing-web` +- 后端 API 服务:`miaojing-api` +- 管理后台服务:`miaojing-console` +- 数据库:PostgreSQL +- 文件持久化:本地存储目录 +- 进程管理:PM2 + +默认路径和端口: + +| 项目 | 默认值 | +| --- | --- | +| 项目部署目录 | `/opt/miaojingAI` | +| 数据存储目录 | `/var/lib/miaojingAI` | +| 前端访问端口 | `5000` | +| 后端 API 内部端口 | `5100` | +| 管理后台内部端口 | `5200` | +| 管理后台访问路径 | `/console` | +| 本地存储目录 | `/var/lib/miaojingAI/storage` | +| 备份目录 | `/var/lib/miaojingAI/backups` | + +## 2. 部署前准备 + +### 2.1 推荐服务器配置与操作系统 + +生产环境推荐使用稳定版 Linux 发行版,不建议直接使用 Windows Server 运行生产服务。Windows 可以作为开发环境,生产环境建议使用 Ubuntu/Debian 系服务器,便于安装 PostgreSQL、PM2、Nginx、Certbot 和系统级守护服务。 + +推荐操作系统: + +| 操作系统 | 推荐版本 | 适用场景 | 说明 | +| --- | --- | --- | --- | +| Ubuntu Server | `24.04 LTS` | 首选生产环境 | 生态成熟,Node.js、PostgreSQL、Nginx、Certbot 支持完整 | +| Ubuntu Server | `26.04 LTS` | 新服务器可选 | 适合全新环境,部署前先在测试机完成一次完整构建和功能验证 | +| Ubuntu Server | `22.04 LTS` | 旧服务器可继续使用 | 仍可运行,但新采购服务器优先选择更新 LTS | +| Debian | `13` | 稳定生产环境 | 当前稳定版,适合长期运行 | +| Debian | `12` | 旧服务器可继续使用 | 已进入旧稳定版周期,可用但新部署优先 Debian 13 | +| CentOS Stream / Rocky Linux / AlmaLinux | `9` | 企业内网环境 | 可用,但脚本和文档示例默认以 Ubuntu/Debian 为主 | + +服务器配置建议: + +| 场景 | CPU | 内存 | 磁盘 | 带宽 | 适用说明 | +| --- | --- | --- | --- | --- | --- | +| 最低测试环境 | 2 核 | 4 GB | 40 GB SSD | 5 Mbps | 仅用于功能验证和少量测试用户,不建议正式上线 | +| 小型生产环境 | 4 核 | 8 GB | 100 GB SSD | 10 Mbps 以上 | 适合早期上线、低到中等访问量,是推荐起步配置 | +| 标准生产环境 | 8 核 | 16 GB | 200 GB SSD | 20 Mbps 以上 | 适合多人同时使用、较多图片/视频结果持久化 | +| 高并发/商用环境 | 16 核以上 | 32 GB 以上 | 500 GB SSD 或独立对象存储 | 50 Mbps 以上 | 建议拆分数据库、对象存储、反向代理和应用服务 | + +磁盘规划建议: + +| 目录 | 推荐大小 | 用途 | +| --- | --- | --- | +| 项目部署目录,如 `/opt/miaojingAI` | 20 GB 以上 | 源码、依赖、构建产物 | +| 数据存储目录,如 `/var/lib/miaojingAI` | 80 GB 以上 | 上传文件、生成结果、备份、部署日志 | +| PostgreSQL 数据目录 | 50 GB 以上 | 用户、作品、订单、配置、日志等业务数据 | + +生产环境基础要求: + +- CPU 架构:`x86_64/amd64` +- Node.js:推荐 `24.x LTS`;可使用 `22.x LTS`;不建议新生产环境继续使用已过维护周期的 Node.js 20 +- PostgreSQL:`16+`,推荐 `17` 或 `18`;最低可用 `14+`,但需要确认仍在安全维护期 +- pnpm:`9.x+` +- PM2:最新版稳定版 +- Nginx:建议用于域名反向代理、HTTPS、静态压缩和访问日志 +- HTTPS:正式上线必须配置有效 TLS 证书 +- 时区:建议设置为 `Asia/Shanghai` +- 防火墙:只开放 `80/443` 和必要的 SSH 端口,`5100/5200` 保持内网访问 +- 应用服务默认绑定 `127.0.0.1`,通过 Nginx 对外提供访问;不要把 `APP_BIND_HOST` 改为 `0.0.0.0`,除非已有上层网络隔离 +- 备份:数据库和 `/var/lib/miaojingAI/storage` 必须有定期离线或异地备份 + +不建议用于正式生产的环境: + +- 非 LTS 版本 Linux,例如 Ubuntu 中间版本;这类系统生命周期短,适合测试,不适合长期生产。 +- 低于 4 核 8 GB 的服务器;Next.js 构建、图片/视频结果持久化和 PostgreSQL 同机运行时容易出现资源不足。 +- 只暴露裸 IP 和 HTTP 端口;正式上线必须使用域名、Nginx 反向代理和 HTTPS。 +- 将数据库、上传文件、生成结果和备份放在项目代码目录内;升级和回滚时容易误删。 +- 使用默认管理员密码、默认数据库密码或公开的 SSH 密码。 + +### 2.2 必需软件 + +部署脚本会自动安装或切换 Node.js 到生产推荐版本,默认使用 `24.x LTS`。如需固定为 `22.x LTS`,执行脚本前设置: + +```bash +DEPLOY_NODE_MAJOR=22 bash scripts/deploy-or-upgrade.sh +``` + +Node.js 会优先从国内可访问镜像源下载,顺序包括 npmmirror、清华、腾讯、华为,最后回退到官方源。默认安装目录为数据目录下的 `node` 子目录,例如 `/var/lib/miaojingAI/node`,不会覆盖系统自带 Node.js。 + +部署脚本会检查以下命令是否存在: + +- `node` / `npm`:没有或版本不符合目标 LTS 时,脚本会自动安装/切换 +- `pnpm` +- `pm2` +- `psql` +- `pg_dump` +- `tar` +- `rsync` +- `curl` + +Ubuntu/Debian 可参考: + +```bash +sudo apt update +sudo apt install -y postgresql-client tar rsync curl +node -v +npm -v +``` + +如果未安装 `pnpm` 或 `pm2`,一键脚本会通过当前 Node.js 对应的 npm 自动安装,并使用国内可访问镜像源。 + +### 2.3 PostgreSQL 数据库 + +部署前需要准备好 PostgreSQL 数据库,并确认服务器可以连接。 + +示例连接地址: + +```text +postgresql://postgres:postgres@localhost:5432/miaojing +``` + +可先手动验证: + +```bash +psql "postgresql://postgres:postgres@localhost:5432/miaojing" -c "SELECT 1;" +``` + +## 3. 首次部署 + +### 3.1 执行一键部署脚本 + +在服务器源码目录执行: + +```bash +bash scripts/deploy-or-upgrade.sh +``` + +脚本会自动检测目标部署目录。如果目标目录没有部署记录,会进入首次部署流程。 + +### 3.2 按提示填写参数 + +首次部署时需要填写: + +| 参数 | 说明 | +| --- | --- | +| 项目部署目录 | 生产运行目录,例如 `/opt/miaojingAI` | +| 数据存储目录 | 持久化数据根目录,例如 `/var/lib/miaojingAI` | +| 前端访问端口 | 浏览器访问端口,例如 `5000` | +| 后端 API 内部端口 | 后端服务端口,例如 `5100` | +| 管理后台内部端口 | 管理后台服务端口,例如 `5200` | +| 管理员账号/昵称 | 管理员登录账号展示名 | +| 管理员邮箱 | 管理员登录邮箱 | +| 管理员密码 | 管理员初始密码 | +| 正式访问地址 | 有域名时填写 `https://域名`,没有域名时可留空使用服务器 IP 和端口 | +| PostgreSQL 连接地址 | 数据库连接字符串 | + +### 3.3 部署完成后的输出 + +部署成功后,脚本会输出: + +- 前台访问地址 +- 管理后台地址 +- 管理员账号 +- 管理员邮箱 +- 管理员密码 +- 项目目录 +- 数据目录 +- 日志文件路径 + +管理后台访问地址示例: + +```text +https://你的域名/console +``` + +只有管理员账号可以登录管理后台。 + +## 4. Nginx、HTTPS 与防火墙 + +正式上线必须使用 Nginx 反向代理和 HTTPS,不建议把 `5000/5100/5200` 直接暴露到公网。仓库已提供生产模板: + +```text +deploy/nginx/miaojing-production.conf +``` + +### 4.1 配置 Nginx + +```bash +sudo cp deploy/nginx/miaojing-production.conf /etc/nginx/sites-available/miaojing.conf +sudo nano /etc/nginx/sites-available/miaojing.conf +sudo ln -sf /etc/nginx/sites-available/miaojing.conf /etc/nginx/sites-enabled/miaojing.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +需要替换模板中的: + +- `example.com` 和 `www.example.com` +- 证书路径 +- 如果一键脚本中修改过前端端口,同步替换 `proxy_pass http://127.0.0.1:5000` + +### 4.2 配置 HTTPS 证书 + +推荐使用 Certbot: + +```bash +sudo apt install -y certbot python3-certbot-nginx +sudo certbot --nginx -d example.com -d www.example.com +sudo certbot renew --dry-run +``` + +### 4.3 防火墙要求 + +生产环境只开放 `80/443` 和必要 SSH 端口,应用内部端口只允许本机访问。 + +Ubuntu UFW 示例: + +```bash +sudo ufw allow OpenSSH +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw deny 5000/tcp +sudo ufw deny 5100/tcp +sudo ufw deny 5200/tcp +sudo ufw enable +sudo ufw status verbose +``` + +云服务器安全组也必须同步只开放 `80/443/SSH`。 + +## 5. 部署后的检查 + +### 5.1 检查 PM2 服务 + +```bash +pm2 list +``` + +正常情况下应看到: + +- `miaojing-web` +- `miaojing-api` +- `miaojing-console` + +状态应为 `online`。 + +### 5.2 检查访问地址 + +```bash +curl -I http://127.0.0.1:5000 +curl -fsS http://127.0.0.1:5000/api/health +curl -I http://127.0.0.1:5000/console +``` + +### 5.3 检查日志 + +```bash +pm2 logs miaojing-web --lines 100 +pm2 logs miaojing-api --lines 100 +pm2 logs miaojing-console --lines 100 +``` + +部署脚本日志位于: + +```text +数据存储目录/logs/deploy-日期时间.log +``` + +示例: + +```text +/var/lib/miaojingAI/logs/deploy-20260503-120000.log +``` + +## 6. 备份操作 + +### 6.1 自动备份 + +执行升级流程时,一键脚本会自动创建升级前备份,备份内容包括: + +- PostgreSQL 数据库 dump +- `.env.local` +- 本地存储文件 +- `package.json` +- 备份清单 `manifest.json` + +默认备份目录: + +```text +/var/lib/miaojingAI/backups +``` + +### 6.2 手动创建备份 + +进入生产部署目录: + +```bash +cd /opt/miaojingAI +pnpm backup:create +``` + +或直接执行: + +```bash +cd /opt/miaojingAI +bash scripts/backup-create.sh +``` + +如果需要指定备份目录: + +```bash +cd /opt/miaojingAI +BACKUP_DIR=/var/lib/miaojingAI/backups bash scripts/backup-create.sh +``` + +脚本成功后会输出备份文件路径,例如: + +```text +/var/lib/miaojingAI/backups/miaojing-backup-20260503-120000.tar.gz +``` + +### 6.3 查看备份文件 + +```bash +ls -lh /var/lib/miaojingAI/backups +``` + +备份脚本默认保留最近 10 个 `miaojing-backup-*.tar.gz` 文件。 + +## 7. 升级操作 + +### 7.1 升级前确认 + +升级前确认: + +- 数据库可连接 +- 当前服务可访问 +- 磁盘空间充足 +- 已拿到新版本源码 +- 没有正在进行的重要生成任务 + +建议检查: + +```bash +df -h +pm2 list +psql "postgresql://postgres:postgres@localhost:5432/miaojing" -c "SELECT 1;" +``` + +### 7.2 执行升级 + +在新版本源码目录执行: + +```bash +bash scripts/deploy-or-upgrade.sh +``` + +当脚本检测到目标部署目录已有 `package.json` 且存在 `.env.local` 或 `.miaojing-deployment` 时,会进入升级流程。 + +升级流程会自动执行: + +1. 读取旧部署配置作为默认值 +2. 创建升级前备份 +3. 迁移旧本地存储到持久化目录 +4. 同步新版本代码到部署目录 +5. 保留 `.env.local` 中原有非部署配置,只更新数据库、端口、持久化目录和密钥等必要项 +6. 补齐数据库结构和索引 +7. 可选更新管理员密码 +8. 安装依赖 +9. 生产构建 +10. 执行生产依赖漏洞扫描;`high/critical` 级别漏洞会阻断上线 +11. 通过 PM2 重载前端、后端、管理后台 +12. 检查 `/api/health` 和 `/console` + +### 7.3 升级参数说明 + +升级时管理员密码可以留空: + +- 留空:不修改管理员密码 +- 输入新密码:更新管理员密码 + +升级不会删除或覆盖以下数据: + +- 用户账号 +- 用户资料 +- 管理员账号 +- 作品记录 +- 积分记录 +- 订单记录 +- 网站配置 +- API 供应商配置 +- 系统 API 配置 +- 用户自定义 API 密钥 +- 支付配置 +- 公告 +- 邮件配置 +- 本地存储文件 + +## 8. 安全与漏洞扫描 + +一键脚本在构建后会执行: + +```bash +pnpm audit --prod --audit-level=high +``` + +发现 `high` 或 `critical` 级别生产依赖漏洞时,脚本会直接失败,必须先升级依赖链并重新构建。`moderate` 级别漏洞会记录在部署日志中,正式上线前仍建议处理。 + +可手动执行完整审计: + +```bash +cd /opt/miaojingAI +pnpm audit --prod --registry=https://registry.npmjs.org +``` + +项目内置的生产安全措施: + +- `/api/admin/clear-users` 默认禁用,只有临时设置 `ENABLE_DANGER_ADMIN_CLEAR_USERS=true` 才能使用。 +- `/console` 管理后台登录要求管理员角色,普通用户不能登录。 +- 登录、注册、邮箱验证码、生成、下载、管理 API 均有应用层基础限流。 +- Nginx 模板提供边缘限流,建议正式生产同时启用应用层和 Nginx 层限流。 +- 全局安全响应头包括 `Content-Security-Policy`、`HSTS`、`X-Content-Type-Options`、`X-Frame-Options`、`Referrer-Policy`、`Permissions-Policy` 和 `Origin-Agent-Cluster`。 +- 生产构建关闭 `X-Powered-By` 技术指纹,并设置 Node HTTP 请求、请求头、Keep-Alive 超时。 +- 下载代理和远程文件持久化会阻断内网、回环和本地地址,降低 SSRF 风险。 +- API Key、SMTP 密码等敏感配置采用服务端加密存储,生产环境必须设置 `DATA_ENCRYPTION_KEY` 和 `JWT_SECRET`。 + +上线前必须确认: + +- `ENABLE_DANGER_ADMIN_CLEAR_USERS=false` +- `.env.local` 权限为 `600` +- 管理员密码不是默认值,也不是弱口令 +- 用户注册密码规则至少 8 位,并同时包含字母和数字 +- 数据库密码不是默认值 +- SSH 禁止公开弱密码,建议使用密钥登录并限制来源 IP +- 云安全组和系统防火墙均未开放 `5000/5100/5200` +- 备份文件目录权限为 `700`,备份文件权限为 `600` + +## 9. 回滚操作 + +### 9.1 使用备份回滚 + +进入生产部署目录: + +```bash +cd /opt/miaojingAI +pnpm backup:restore /var/lib/miaojingAI/backups/miaojing-backup-YYYYMMDD-HHMMSS.tar.gz +``` + +或直接执行: + +```bash +cd /opt/miaojingAI +bash scripts/backup-restore.sh /var/lib/miaojingAI/backups/miaojing-backup-YYYYMMDD-HHMMSS.tar.gz +``` + +回滚会恢复: + +- 数据库 +- `.env.local` +- 本地存储文件 + +### 9.2 回滚后重启服务 + +```bash +cd /opt/miaojingAI +pm2 startOrReload ecosystem.config.cjs --update-env +pm2 save +``` + +### 9.3 回滚后验证 + +```bash +curl -fsS http://127.0.0.1:5000/api/health +curl -I http://127.0.0.1:5000/console +pm2 list +``` + +## 10. 数据持久化说明 + +生产部署中,代码目录和数据目录分离。 + +代码目录示例: + +```text +/opt/miaojingAI +``` + +数据目录示例: + +```text +/var/lib/miaojingAI +``` + +关键持久化路径: + +| 数据 | 路径 | +| --- | --- | +| 本地上传和生成文件 | `/var/lib/miaojingAI/storage` | +| 备份文件 | `/var/lib/miaojingAI/backups` | +| 部署日志 | `/var/lib/miaojingAI/logs` | +| 生产环境变量 | `/opt/miaojingAI/.env.local` | +| 部署标记 | `/opt/miaojingAI/.miaojing-deployment` | + +升级同步代码时会排除: + +- `.env.local` +- `node_modules` +- `.next` +- `dist` +- `backups` +- `local-storage` +- `.git` +- `.codex_tmp` + +因此正常升级不会覆盖用户数据和持久化文件。 + +## 11. 常用运维命令 + +### 11.1 查看服务状态 + +```bash +pm2 list +``` + +### 11.2 查看服务日志 + +```bash +pm2 logs miaojing-web --lines 100 +pm2 logs miaojing-api --lines 100 +pm2 logs miaojing-console --lines 100 +``` + +### 11.3 重启服务 + +```bash +cd /opt/miaojingAI +pm2 startOrReload ecosystem.config.cjs --update-env +pm2 save +``` + +### 11.4 停止服务 + +```bash +pm2 stop miaojing-web miaojing-api miaojing-console +``` + +### 11.5 启动服务 + +```bash +cd /opt/miaojingAI +pm2 start ecosystem.config.cjs --update-env +pm2 save +``` + +### 11.6 查看部署日志 + +```bash +ls -lh /var/lib/miaojingAI/logs +tail -n 200 /var/lib/miaojingAI/logs/deploy-*.log +``` + +## 12. 常见问题处理 + +### 12.1 Node.js 自动安装失败 + +脚本默认会自动安装或切换到 Node.js `24.x LTS`。如果服务器无法访问所有 Node.js 镜像源,会提示安装失败。 + +```bash +node -v +curl -I https://npmmirror.com/mirrors/node/index.json +``` + +可改用 Node.js `22.x LTS`: + +```bash +DEPLOY_NODE_MAJOR=22 bash scripts/deploy-or-upgrade.sh +``` + +### 12.2 依赖安装失败 + +脚本会依次尝试以下源: + +- `https://registry.npmmirror.com` +- `https://registry.npmjs.org` +- `https://mirrors.cloud.tencent.com/npm/` +- `https://mirrors.huaweicloud.com/repository/npm/` + +如果全部失败,检查服务器网络和 DNS。 + +### 12.3 数据库连接失败 + +检查连接地址: + +```bash +psql "postgresql://postgres:postgres@localhost:5432/miaojing" -c "SELECT 1;" +``` + +检查 PostgreSQL 服务: + +```bash +systemctl status postgresql +``` + +### 12.4 健康检查失败 + +查看 PM2 日志: + +```bash +pm2 logs miaojing-web --lines 120 +pm2 logs miaojing-api --lines 120 +pm2 logs miaojing-console --lines 120 +``` + +检查端口是否被占用: + +```bash +ss -lntp | grep -E ':5000|:5100|:5200' +``` + +### 12.5 管理后台无法登录 + +确认访问路径: + +```text +http://服务器IP:5000/console +``` + +确认账号为管理员角色: + +```sql +SELECT id, email, nickname, role, is_active FROM profiles WHERE role = 'admin'; +``` + +升级时如果需要重置管理员密码,重新执行: + +```bash +bash scripts/deploy-or-upgrade.sh +``` + +在提示“管理员密码(升级时可留空表示不修改)”时输入新密码。 + +### 12.6 作品图片或视频无法访问 + +检查 `.env.local` 中的本地存储目录: + +```bash +grep LOCAL_STORAGE_DIR /opt/miaojingAI/.env.local +``` + +检查文件目录: + +```bash +ls -lh /var/lib/miaojingAI/storage +``` + +升级脚本会自动将旧部署目录中的 `local-storage` 迁移到持久化目录。 + +## 13. 上线检查清单 + +上线前逐项确认: + +- 数据库连接正常 +- 一键部署脚本执行成功 +- `pm2 list` 三个服务均为 `online` +- 首页可访问 +- `/api/health` 返回正常 +- `/console` 可访问 +- 管理员可登录后台 +- 网站设置可读取和保存 +- API 管理配置可读取和保存 +- 用户注册、登录正常 +- 创作作品可以生成、保存和访问 +- 本地存储目录存在且可写 +- 手动备份可以成功生成 +- 升级前备份路径已记录 +- Nginx 已配置域名和 HTTPS +- 系统防火墙和云安全组只开放 `80/443/SSH` +- `5000/5100/5200` 未对公网开放 +- `pnpm audit --prod` 无 `high/critical` 漏洞 +- `.env.local` 中 `JWT_SECRET`、`DATA_ENCRYPTION_KEY`、`GENERATION_INTERNAL_SECRET` 均已设置 +- `ENABLE_DANGER_ADMIN_CLEAR_USERS=false` +- `/console` 响应头不包含 `X-Powered-By`,并包含 `Content-Security-Policy` +- 管理后台“系统日志”可正常筛选查看 +- 管理后台仪表盘中数据库、存储、日志状态正常 + +## 14. 关键文件 + +| 文件 | 用途 | +| --- | --- | +| `scripts/deploy-or-upgrade.sh` | 一键部署和升级 | +| `scripts/backup-create.sh` | 创建备份 | +| `scripts/backup-restore.sh` | 恢复备份 | +| `scripts/init-database.sql` | 初始化和补齐数据库结构 | +| `scripts/database-optimization-patch.sql` | 数据库优化补丁 | +| `scripts/start.sh` | PM2 调用的启动脚本 | +| `deploy/nginx/miaojing-production.conf` | Nginx 生产反向代理模板 | +| `.env.local` | 生产环境变量 | +| `ecosystem.config.cjs` | PM2 进程配置 | diff --git a/account-profile-migration.sql b/account-profile-migration.sql new file mode 100644 index 0000000..d9d2bf5 --- /dev/null +++ b/account-profile-migration.sql @@ -0,0 +1 @@ +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS password_hash TEXT; diff --git a/assets/image.png b/assets/image.png new file mode 100644 index 0000000..23f8162 Binary files /dev/null and b/assets/image.png differ diff --git a/assets/miaojinglogo.png b/assets/miaojinglogo.png new file mode 100644 index 0000000..0372b13 Binary files /dev/null and b/assets/miaojinglogo.png differ diff --git a/audit_current_persistence.js b/audit_current_persistence.js new file mode 100644 index 0000000..1a2a990 --- /dev/null +++ b/audit_current_persistence.js @@ -0,0 +1,204 @@ +const { Client } = require('pg'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); + +const root = '/root/miaojingAI'; +const client = new Client({ connectionString: process.env.LOCAL_DB_URL }); + +function storageKeyFromUrl(url) { + if (!url || typeof url !== 'string') return null; + const marker = '/api/local-storage/'; + const index = url.indexOf(marker); + if (index < 0) return null; + try { + return decodeURIComponent(url.slice(index + marker.length).split('?')[0]); + } catch { + return null; + } +} + +function countFiles(dir) { + if (!fs.existsSync(dir)) return 0; + let count = 0; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + count += entry.isDirectory() ? countFiles(full) : 1; + } + return count; +} + +async function q(name, sql) { + const result = await client.query(sql); + return { name, rows: result.rows }; +} + +(async () => { + await client.connect(); + const checks = {}; + + checks.tableCounts = await q('table_counts', ` + select 'auth.users' as table_name, count(*)::int as count from auth.users + union all select 'profiles', count(*)::int from profiles + union all select 'user_api_keys', count(*)::int from user_api_keys + union all select 'works', count(*)::int from works + union all select 'generation_jobs', count(*)::int from generation_jobs + union all select 'credit_transactions', count(*)::int from credit_transactions + union all select 'orders', count(*)::int from orders + union all select 'api_providers', count(*)::int from api_providers + union all select 'model_recommendations', count(*)::int from model_recommendations + `); + + checks.profiles = await q('profiles', ` + select id, email, nickname, role, membership_tier, credits_balance, is_active, + avatar_url is not null as has_avatar, created_at, updated_at + from profiles + order by created_at desc + `); + + checks.profileAuthIntegrity = await q('profile_auth_integrity', ` + select 'profile_without_auth' as issue, count(*)::int as count + from profiles p left join auth.users u on u.id = p.id where u.id is null + union all + select 'auth_without_profile', count(*)::int + from auth.users u left join profiles p on p.id = u.id where p.id is null + `); + + checks.admin = await q('admin', ` + select p.id, p.email, p.nickname, p.role, p.membership_tier, p.is_active, + u.password_hash is not null as has_password_hash + from profiles p + left join auth.users u on u.id = p.id + where p.role = 'admin' or p.email = 'admin@example.com' + order by p.created_at + `); + + checks.apiKeys = await q('api_keys', ` + select id, user_id, provider, supplier_name, type, model_name, note, + api_key_preview, length(api_key_encrypted) as encrypted_len, + api_key_encrypted like 'mjenc:v1:%' as has_enc_prefix, + api_key_encrypted = api_key_preview as encrypted_equals_preview, + is_active, created_at + from user_api_keys + order by created_at desc + `); + + checks.apiKeyIntegrity = await q('api_key_integrity', ` + select count(*)::int as orphan_api_keys + from user_api_keys k left join profiles p on p.id = k.user_id + where p.id is null + `); + + checks.worksDistribution = await q('works_distribution', ` + select type, status, is_public, count(*)::int as count, + count(*) filter (where result_url like '/api/local-storage/%')::int as local_urls, + count(*) filter (where result_url like 'data:%')::int as data_urls, + count(*) filter (where result_url like '[%')::int as placeholder_urls + from works + group by type, status, is_public + order by type, status, is_public + `); + + checks.worksByUser = await q('works_by_user', ` + select p.email, p.nickname, w.user_id, count(*)::int as works + from works w + left join profiles p on p.id = w.user_id + group by p.email, p.nickname, w.user_id + order by works desc nulls last + `); + + checks.workIntegrity = await q('work_integrity', ` + select 'works_missing_user' as issue, count(*)::int as count + from works w left join profiles p on p.id = w.user_id + where p.id is null + union all + select 'works_null_user', count(*)::int from works where user_id is null + union all + select 'works_bad_url', count(*)::int + from works + where result_url is null or result_url = '' or result_url like 'data:%' + union all + select 'public_not_gallery_url', count(*)::int + from works + where is_public = true and result_url not like '/api/local-storage/gallery/%' + `); + + checks.reversePromptWorks = await q('reverse_prompt_works', ` + select id, user_id, type, + left(result_url, 100) as result_url, + left(coalesce(params->>'referenceImage', ''), 100) as reference_image, + prompt <> '' as has_prompt, + negative_prompt is not null as has_negative_prompt, + created_at + from works + where type = 'reverse-prompt' + order by created_at desc + limit 20 + `); + + checks.galleryWorks = await q('gallery_works', ` + select id, user_id, type, is_public, left(result_url, 100) as result_url, created_at + from works + where is_public = true + order by created_at desc + `); + + checks.generationJobs = await q('generation_jobs', ` + select status, count(*)::int as count, + count(*) filter (where user_id is null)::int as null_user_count + from generation_jobs + group by status + order by status + `); + + checks.generationJobsByUser = await q('generation_jobs_by_user', ` + select p.email, p.nickname, j.user_id, count(*)::int as jobs + from generation_jobs j + left join profiles p on p.id = j.user_id + group by p.email, p.nickname, j.user_id + order by jobs desc nulls last + `); + + const works = (await client.query(` + select id, result_url, thumbnail_url, params + from works + order by created_at desc + `)).rows; + + const missingFiles = []; + let referencedLocalUrls = 0; + for (const work of works) { + const urls = [ + ['result_url', work.result_url], + ['thumbnail_url', work.thumbnail_url], + ['referenceImage', work.params && work.params.referenceImage], + ]; + if (work.params && Array.isArray(work.params.referenceImages)) { + work.params.referenceImages.forEach((url, index) => urls.push([`referenceImages[${index}]`, url])); + } + for (const [field, url] of urls) { + const key = storageKeyFromUrl(url); + if (!key) continue; + referencedLocalUrls++; + const full = path.join(root, 'local-storage', key); + if (!fs.existsSync(full)) missingFiles.push({ workId: work.id, field, key }); + } + } + + checks.storage = { + localStorageExists: fs.existsSync(path.join(root, 'local-storage')), + backupsExists: fs.existsSync(path.join(root, 'backups')), + localFiles: countFiles(path.join(root, 'local-storage')), + generatedFiles: countFiles(path.join(root, 'local-storage', 'generated')), + galleryFiles: countFiles(path.join(root, 'local-storage', 'gallery')), + reversePromptFiles: countFiles(path.join(root, 'local-storage', 'reverse-prompt')), + referencedLocalUrls, + missingFiles, + }; + + console.log(JSON.stringify(checks, null, 2)); + await client.end(); +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/audit_db_storage.js b/audit_db_storage.js new file mode 100644 index 0000000..9defd1f --- /dev/null +++ b/audit_db_storage.js @@ -0,0 +1,67 @@ +const { Client } = require('pg'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); +const c = new Client({ connectionString: process.env.LOCAL_DB_URL }); +const root = '/root/miaojingAI'; +function storageKeyFromUrl(url) { + if (!url || typeof url !== 'string') return null; + const marker = '/api/local-storage/'; + const idx = url.indexOf(marker); + if (idx < 0) return null; + try { return decodeURIComponent(url.slice(idx + marker.length).split('?')[0]); } catch { return null; } +} +async function query(name, sql, params=[]) { + try { const r = await c.query(sql, params); return { name, rows: r.rows }; } + catch (e) { return { name, error: e.message }; } +} +(async () => { + await c.connect(); + const out = {}; + out.db = await query('db', `select current_database() as database, current_user as user, now() as checked_at`); + out.tables = await query('tables', `select schemaname, tablename from pg_tables where schemaname in ('public','auth') order by schemaname, tablename`); + const tableNames = out.tables.rows || []; + out.counts = []; + for (const t of tableNames) { + const full = t.schemaname === 'public' ? t.tablename : `${t.schemaname}.${t.tablename}`; + const r = await query(full, `select count(*)::int as count from ${full}`); + out.counts.push({ table: full, count: r.rows?.[0]?.count ?? null, error: r.error }); + } + out.admins = await query('admins', `select id,email,nickname,role,membership_tier,is_active,created_at,updated_at from profiles where role='admin' or email in ('admin@example.com','admin@miaojing.ai') order by created_at`); + out.authAdmins = await query('auth_admins', `select id,email,created_at, password_hash is not null as has_password from auth.users where email in ('admin@example.com','admin@miaojing.ai') order by created_at`); + out.profileWithoutAuth = await query('profile_without_auth', `select p.id,p.email,p.role from profiles p left join auth.users u on u.id=p.id where u.id is null order by p.created_at limit 50`); + out.authWithoutProfile = await query('auth_without_profile', `select u.id,u.email from auth.users u left join profiles p on p.id=u.id where p.id is null order by u.created_at limit 50`); + out.worksMissingUser = await query('works_missing_user', `select w.id,w.user_id,w.title,w.type,w.status,w.created_at from works w left join profiles p on p.id=w.user_id where w.user_id is not null and p.id is null order by w.created_at desc limit 50`); + out.worksNullUser = await query('works_null_user', `select id,title,type,status,created_at from works where user_id is null order by created_at desc limit 50`); + out.worksByStatus = await query('works_by_status', `select status, count(*)::int from works group by status order by status`); + out.worksByUser = await query('works_by_user', `select p.email,p.nickname,w.user_id,count(*)::int as works from works w left join profiles p on p.id=w.user_id group by p.email,p.nickname,w.user_id order by works desc nulls last limit 20`); + out.workLikesMissing = await query('work_likes_missing_refs', `select wl.id,wl.user_id,wl.work_id from work_likes wl left join profiles p on p.id=wl.user_id left join works w on w.id=wl.work_id where p.id is null or w.id is null limit 50`); + out.creditMissingUser = await query('credit_missing_user', `select ct.id,ct.user_id,ct.amount,ct.type,ct.created_at from credit_transactions ct left join profiles p on p.id=ct.user_id where p.id is null limit 50`); + out.ordersMissingUser = await query('orders_missing_user', `select o.id,o.user_id,o.order_no,o.status,o.created_at from orders o left join profiles p on p.id=o.user_id where o.user_id is not null and p.id is null limit 50`); + out.apiKeysMissingUser = await query('api_keys_missing_user', `select k.id,k.user_id,k.provider,k.type from user_api_keys k left join profiles p on p.id=k.user_id where p.id is null limit 50`); + out.jobsMissingUser = await query('jobs_missing_user', `select j.id,j.user_id,j.type,j.status,j.created_at from generation_jobs j left join profiles p on p.id=j.user_id where j.user_id is not null and p.id is null order by j.created_at desc limit 50`); + out.jobsNullUser = await query('jobs_null_user', `select id,type,status,created_at from generation_jobs where user_id is null order by created_at desc limit 50`); + out.jobsByUser = await query('jobs_by_user', `select p.email,p.nickname,j.user_id,count(*)::int as jobs from generation_jobs j left join profiles p on p.id=j.user_id group by p.email,p.nickname,j.user_id order by jobs desc nulls last limit 20`); + out.worksFileCheck = { totalLocalUrls: 0, missing: [] }; + const works = await query('works_urls', `select id,result_url,thumbnail_url from works order by created_at desc`); + for (const w of works.rows || []) { + for (const field of ['result_url','thumbnail_url']) { + const key = storageKeyFromUrl(w[field]); + if (!key) continue; + out.worksFileCheck.totalLocalUrls++; + const abs = path.join(root, 'local-storage', key); + if (!fs.existsSync(abs)) out.worksFileCheck.missing.push({ workId: w.id, field, key }); + } + } + out.storage = { + localStorageExists: fs.existsSync(path.join(root,'local-storage')), + backupsExists: fs.existsSync(path.join(root,'backups')), + localFiles: 0, + galleryFiles: 0, + }; + function walk(dir) { if (!fs.existsSync(dir)) return 0; let n=0; for (const e of fs.readdirSync(dir,{withFileTypes:true})) { const p=path.join(dir,e.name); n += e.isDirectory()?walk(p):1; } return n; } + out.storage.localFiles = walk(path.join(root,'local-storage')); + out.storage.galleryFiles = walk(path.join(root,'local-storage','gallery')); + console.log(JSON.stringify(out, null, 2)); + await c.end(); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/audit_recovered_data.js b/audit_recovered_data.js new file mode 100644 index 0000000..02daf34 --- /dev/null +++ b/audit_recovered_data.js @@ -0,0 +1,206 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + +function short(value, length = 160) { + if (value == null) return value; + const text = typeof value === 'string' ? value : JSON.stringify(value); + return text.length > length ? `${text.slice(0, length)}...` : text; +} + +async function main() { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const tableColumns = await client.query(` + SELECT table_schema, table_name, column_name, data_type + FROM information_schema.columns + WHERE (table_schema = 'public' AND table_name IN ('profiles', 'works', 'user_api_keys', 'orders', 'credit_transactions')) + OR (table_schema = 'auth' AND table_name = 'users') + ORDER BY table_schema, table_name, ordinal_position + `); + + const userSummary = await client.query(` + SELECT + (SELECT COUNT(*)::int FROM auth.users) AS auth_users, + (SELECT COUNT(*)::int FROM profiles) AS profiles, + (SELECT COUNT(*)::int FROM profiles WHERE role = 'admin') AS admins, + (SELECT COUNT(*)::int FROM profiles WHERE role <> 'admin') AS non_admin_profiles, + (SELECT COUNT(*)::int FROM auth.users u LEFT JOIN profiles p ON p.id = u.id WHERE p.id IS NULL) AS auth_without_profile, + (SELECT COUNT(*)::int FROM profiles p LEFT JOIN auth.users u ON u.id = p.id WHERE u.id IS NULL) AS profile_without_auth, + (SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NULL OR password_hash = '') AS auth_without_password_hash, + (SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NOT NULL AND password_hash <> '') AS auth_with_password_hash + `); + + const userSamples = await client.query(` + SELECT p.id, p.email, p.nickname, p.role, p.membership_tier, p.is_active, + u.id IS NOT NULL AS has_auth, + (u.password_hash IS NOT NULL AND u.password_hash <> '') AS has_password_hash, + p.created_at + FROM profiles p + LEFT JOIN auth.users u ON u.id = p.id + ORDER BY p.created_at DESC NULLS LAST + LIMIT 80 + `); + + const workSummary = await client.query(` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE status = 'completed')::int AS completed, + COUNT(*) FILTER (WHERE is_public = true AND status = 'completed')::int AS public_completed, + COUNT(*) FILTER (WHERE is_public = false AND status = 'completed')::int AS private_completed, + COUNT(*) FILTER (WHERE user_id IS NULL)::int AS null_user_id, + COUNT(*) FILTER (WHERE user_id = $1)::int AS system_user_id, + COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile, + COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + `, [SYSTEM_USER_ID]); + + const publicWorkSummary = await client.query(` + SELECT + COUNT(*)::int AS public_total, + COUNT(*) FILTER (WHERE w.user_id IS NULL)::int AS null_user_id, + COUNT(*) FILTER (WHERE w.user_id = $1)::int AS system_user_id, + COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile, + COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE w.is_public = true AND w.status = 'completed' + `, [SYSTEM_USER_ID]); + + const workByUser = await client.query(` + SELECT + COALESCE(p.email, '[missing-profile]') AS email, + COALESCE(p.nickname, '') AS nickname, + COALESCE(p.role, '') AS role, + w.user_id, + COUNT(*)::int AS total_works, + COUNT(*) FILTER (WHERE w.status = 'completed')::int AS completed_works, + COUNT(*) FILTER (WHERE w.is_public = true AND w.status = 'completed')::int AS public_works, + COUNT(*) FILTER (WHERE w.is_public = false AND w.status = 'completed')::int AS history_works + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + GROUP BY w.user_id, p.email, p.nickname, p.role + ORDER BY total_works DESC + LIMIT 120 + `); + + const orphanSamples = await client.query(` + SELECT w.id, w.user_id, w.type, w.status, w.is_public, w.result_url, + LEFT(COALESCE(w.prompt, ''), 140) AS prompt, + w.params, + w.created_at + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL + ORDER BY w.created_at DESC NULLS LAST + LIMIT 80 + `, [SYSTEM_USER_ID]); + + const paramKeys = await client.query(` + SELECT key, COUNT(*)::int AS count + FROM works w + CROSS JOIN LATERAL jsonb_object_keys(COALESCE(w.params, '{}'::jsonb)) AS key + LEFT JOIN profiles p ON p.id = w.user_id + WHERE w.is_public = true + AND w.status = 'completed' + AND (p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL) + GROUP BY key + ORDER BY count DESC, key + LIMIT 80 + `, [SYSTEM_USER_ID]); + + const possibleOwnerFields = await client.query(` + SELECT + id, + user_id, + params->>'user_id' AS params_user_id, + params->>'userId' AS params_user_id_camel, + params->>'publisher_id' AS publisher_id, + params->>'publisherId' AS publisher_id_camel, + params->>'owner_id' AS owner_id, + params->>'ownerId' AS owner_id_camel, + params->>'created_by' AS created_by, + params->>'createdBy' AS created_by_camel, + params->>'email' AS params_email, + params->>'userEmail' AS params_user_email, + params->>'publisherEmail' AS params_publisher_email, + params->>'nickname' AS params_nickname, + params->>'userName' AS params_user_name, + LEFT(COALESCE(prompt, ''), 120) AS prompt + FROM works + WHERE is_public = true + AND status = 'completed' + AND (user_id IS NULL OR user_id = $1 OR NOT EXISTS (SELECT 1 FROM profiles p WHERE p.id = works.user_id)) + ORDER BY created_at DESC NULLS LAST + LIMIT 80 + `, [SYSTEM_USER_ID]); + + const duplicateCandidates = await client.query(` + SELECT + public.id AS orphan_id, + public.user_id AS orphan_user_id, + private.id AS owned_id, + private.user_id AS owner_user_id, + p.email, + p.nickname, + CASE + WHEN private.result_url = public.result_url THEN 'result_url' + WHEN COALESCE(private.thumbnail_url, '') <> '' AND private.thumbnail_url = public.thumbnail_url THEN 'thumbnail_url' + WHEN COALESCE(private.prompt, '') <> '' AND private.prompt = public.prompt THEN 'prompt_time' + ELSE 'unknown' + END AS match_type, + ABS(EXTRACT(EPOCH FROM (private.created_at - public.created_at)))::int AS seconds_apart, + LEFT(COALESCE(public.prompt, ''), 120) AS prompt + FROM works public + JOIN works private + ON private.id <> public.id + AND private.user_id IS NOT NULL + AND private.user_id <> $1 + AND ( + private.result_url = public.result_url + OR ( + COALESCE(public.thumbnail_url, '') <> '' + AND private.thumbnail_url = public.thumbnail_url + ) + OR ( + COALESCE(private.prompt, '') <> '' + AND private.prompt = public.prompt + AND private.created_at BETWEEN public.created_at - INTERVAL '30 minutes' AND public.created_at + INTERVAL '30 minutes' + ) + ) + JOIN profiles p ON p.id = private.user_id + LEFT JOIN profiles public_profile ON public_profile.id = public.user_id + WHERE public.is_public = true + AND public.status = 'completed' + AND (public_profile.id IS NULL OR public.user_id = $1 OR public.user_id IS NULL) + ORDER BY public.created_at DESC NULLS LAST, match_type, seconds_apart + LIMIT 100 + `, [SYSTEM_USER_ID]); + + const output = { + columns: tableColumns.rows, + userSummary: userSummary.rows[0], + userSamples: userSamples.rows, + workSummary: workSummary.rows[0], + publicWorkSummary: publicWorkSummary.rows[0], + workByUser: workByUser.rows, + orphanSamples: orphanSamples.rows.map(row => ({ ...row, result_url: short(row.result_url, 120), params: short(row.params, 300) })), + anonymousParamKeys: paramKeys.rows, + possibleOwnerFields: possibleOwnerFields.rows, + duplicateCandidates: duplicateCandidates.rows, + }; + + console.log(JSON.stringify(output, null, 2)); + } finally { + client.release(); + await pool.end(); + } +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/backfill_gallery_user_links.js b/backfill_gallery_user_links.js new file mode 100644 index 0000000..289b201 --- /dev/null +++ b/backfill_gallery_user_links.js @@ -0,0 +1,64 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + const result = await client.query(` + WITH candidates AS ( + SELECT DISTINCT ON (public.id) + public.id AS public_id, + private.user_id AS owner_user_id, + CASE + WHEN private.result_url = public.result_url THEN 1 + WHEN private.thumbnail_url = public.thumbnail_url THEN 2 + ELSE 3 + END AS confidence_rank, + ABS(EXTRACT(EPOCH FROM (private.created_at - public.created_at))) AS time_distance + FROM works public + JOIN works private + ON private.id <> public.id + AND private.user_id <> $1 + AND ( + private.result_url = public.result_url + OR ( + public.thumbnail_url IS NOT NULL + AND private.thumbnail_url = public.thumbnail_url + ) + OR ( + COALESCE(private.prompt, '') = COALESCE(public.prompt, '') + AND private.created_at BETWEEN public.created_at - INTERVAL '10 minutes' AND public.created_at + INTERVAL '10 minutes' + ) + ) + JOIN profiles p ON p.id = private.user_id + WHERE public.is_public = true + AND public.status = 'completed' + AND public.user_id = $1 + ORDER BY public.id, confidence_rank, time_distance + ), + updated AS ( + UPDATE works w + SET user_id = candidates.owner_user_id + FROM candidates + WHERE w.id = candidates.public_id + RETURNING w.id, w.user_id + ) + SELECT COUNT(*)::int AS fixed_count FROM updated + `, [SYSTEM_USER_ID]); + await client.query('COMMIT'); + console.log(JSON.stringify(result.rows[0] || { fixed_count: 0 }, null, 2)); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/backfill_generation_jobs_user.js b/backfill_generation_jobs_user.js new file mode 100644 index 0000000..2bf2a82 --- /dev/null +++ b/backfill_generation_jobs_user.js @@ -0,0 +1,46 @@ +const { Client } = require('pg'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); + +(async () => { + const client = new Client({ connectionString: process.env.LOCAL_DB_URL }); + await client.connect(); + try { + await client.query('BEGIN'); + const admin = await client.query( + `SELECT id FROM profiles WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1`, + ); + if (!admin.rows[0]?.id) { + throw new Error('No admin profile found'); + } + const result = await client.query( + `UPDATE generation_jobs + SET user_id = $1 + WHERE user_id IS NULL + RETURNING id`, + [admin.rows[0].id], + ); + await client.query('COMMIT'); + + const summary = await client.query( + `SELECT status, + count(*)::int AS count, + count(*) FILTER (WHERE user_id IS NULL)::int AS null_user_count + FROM generation_jobs + GROUP BY status + ORDER BY status`, + ); + console.log(JSON.stringify({ + backfilled: result.rowCount, + adminUserId: admin.rows[0].id, + summary: summary.rows, + }, null, 2)); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + await client.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/cleanup_import_edge_cases.js b/cleanup_import_edge_cases.js new file mode 100644 index 0000000..9d99e7d --- /dev/null +++ b/cleanup_import_edge_cases.js @@ -0,0 +1,73 @@ +const fs = require('fs'); +const path = require('path'); +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +function collectLocalStorageKeys(value, keys = new Set()) { + if (typeof value === 'string') { + const marker = '/api/local-storage/'; + const index = value.indexOf(marker); + if (index >= 0) { + keys.add(decodeURIComponent(value.slice(index + marker.length).split('?')[0])); + } + return keys; + } + if (Array.isArray(value)) { + value.forEach(item => collectLocalStorageKeys(item, keys)); + return keys; + } + if (value && typeof value === 'object') { + Object.values(value).forEach(item => collectLocalStorageKeys(item, keys)); + } + return keys; +} + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const works = await client.query( + `SELECT id, result_url, thumbnail_url, params + FROM works + WHERE title LIKE 'codex-import-edge-%' OR prompt LIKE 'codex-import-edge-%'`, + ); + const keys = new Set(); + for (const row of works.rows) { + collectLocalStorageKeys(row.result_url, keys); + collectLocalStorageKeys(row.thumbnail_url, keys); + collectLocalStorageKeys(row.params, keys); + } + + const deletedWorks = await client.query( + `DELETE FROM works + WHERE title LIKE 'codex-import-edge-%' OR prompt LIKE 'codex-import-edge-%'`, + ); + const deletedAnnouncements = await client.query( + `DELETE FROM announcements + WHERE title LIKE 'codex-import-edge-%'`, + ); + + let deletedFiles = 0; + const base = path.join(process.cwd(), 'local-storage'); + for (const key of keys) { + const filePath = path.normalize(path.join(base, key)); + if (!filePath.startsWith(base)) continue; + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + deletedFiles += 1; + } + } + + console.log(JSON.stringify({ + deletedWorks: deletedWorks.rowCount || 0, + deletedAnnouncements: deletedAnnouncements.rowCount || 0, + deletedFiles, + }, null, 2)); + } finally { + client.release(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/components.json b/components.json new file mode 100644 index 0000000..3289f23 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/deploy/nginx/miaojing-production.conf b/deploy/nginx/miaojing-production.conf new file mode 100644 index 0000000..715d583 --- /dev/null +++ b/deploy/nginx/miaojing-production.conf @@ -0,0 +1,109 @@ +# 妙境 AI 创作平台 Nginx 生产配置模板 +# 使用前替换: +# - example.com +# - /etc/letsencrypt/live/example.com/fullchain.pem +# - /etc/letsencrypt/live/example.com/privkey.pem +# - 如脚本中修改了前端端口,请同步 proxy_pass 的 5000 + +limit_req_zone $binary_remote_addr zone=miaojing_auth:10m rate=10r/m; +limit_req_zone $binary_remote_addr zone=miaojing_email:10m rate=6r/m; +limit_req_zone $binary_remote_addr zone=miaojing_generation:10m rate=20r/m; +limit_req_zone $binary_remote_addr zone=miaojing_download:10m rate=60r/m; +limit_req_zone $binary_remote_addr zone=miaojing_admin:10m rate=120r/m; + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name example.com www.example.com; + + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name example.com www.example.com; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + + client_max_body_size 80m; + keepalive_timeout 65; + proxy_connect_timeout 60s; + proxy_send_timeout 360s; + proxy_read_timeout 360s; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + + access_log /var/log/nginx/miaojing-access.log; + error_log /var/log/nginx/miaojing-error.log warn; + + location = /api/auth/login { + limit_req zone=miaojing_auth burst=20 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location = /api/auth/register { + limit_req zone=miaojing_auth burst=20 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location ^~ /api/email/ { + limit_req zone=miaojing_email burst=10 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location ^~ /api/generate/ { + limit_req zone=miaojing_generation burst=30 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location = /api/download { + limit_req zone=miaojing_download burst=120 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location ^~ /api/admin/ { + limit_req zone=miaojing_admin burst=120 nodelay; + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_set_header X-Forwarded-Proto https; + } + + location / { + proxy_pass http://127.0.0.1:5000; + include proxy_params; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-Proto https; + } +} diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..09e9a00 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,54 @@ +module.exports = { + apps: [ + { + name: 'miaojing-api', + cwd: '/root/miaojingAI', + script: 'npm', + args: 'run start', + exec_mode: 'fork', + instances: 1, + max_memory_restart: '512M', + restart_delay: 3000, + env: { + NODE_ENV: 'production', + COZE_PROJECT_ENV: 'PROD', + APP_RUNTIME_ROLE: 'backend', + DEPLOY_RUN_PORT: '5100', + }, + }, + { + name: 'miaojing-web', + cwd: '/root/miaojingAI', + script: 'npm', + args: 'run start', + exec_mode: 'fork', + instances: 1, + max_memory_restart: '512M', + restart_delay: 3000, + env: { + NODE_ENV: 'production', + COZE_PROJECT_ENV: 'PROD', + APP_RUNTIME_ROLE: 'frontend', + BACKEND_INTERNAL_URL: 'http://127.0.0.1:5100', + CONSOLE_INTERNAL_URL: 'http://127.0.0.1:5200', + DEPLOY_RUN_PORT: '5000', + }, + }, + { + name: 'miaojing-console', + cwd: '/root/miaojingAI', + script: 'npm', + args: 'run start', + exec_mode: 'fork', + instances: 1, + max_memory_restart: '512M', + restart_delay: 3000, + env: { + NODE_ENV: 'production', + COZE_PROJECT_ENV: 'PROD', + APP_RUNTIME_ROLE: 'console', + DEPLOY_RUN_PORT: '5200', + }, + }, + ], +}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..a97ed47 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,52 @@ +import nextTs from 'eslint-config-next/typescript'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import { defineConfig, globalIgnores } from 'eslint/config'; + +const syntaxRules = [ + { + selector: 'JSXOpeningElement[name.name="head"]', + message: + '禁止使用 head 标签,优先使用 metadata。三方 CSS、字体等资源可以在 globals.css 中顶部通过 @import 引入或者使用 next/font;preload, preconnect, dns-prefetch 通过 ReactDOM 的 preload、preconnect、dns-prefetch 方法引入;json-ld 可阅读 https://nextjs.org/docs/app/guides/json-ld', + }, +]; + +const nextConfigRestrictedSyntaxRules = [ + { + selector: + 'Property[key.name=/^(root|outputFileTracingRoot)$/] > Literal[value=/^\\//]', + message: + '禁止在 next.config 中写死绝对路径,请改用 path.resolve(__dirname, ...)、import.meta.dirname 或 process.cwd() 动态拼接。', + }, +]; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + { + rules: { + 'react-hooks/set-state-in-effect': 'off', + 'no-restricted-syntax': ['error', ...syntaxRules], + }, + }, + { + files: ['next.config.ts'], + rules: { + 'no-restricted-syntax': ['error', ...nextConfigRestrictedSyntaxRules], + }, + }, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', + // Build artifacts: + 'server.js', + 'dist/**', + // Script files (CommonJS): + 'scripts/**/*.js', + ]), +]); + +export default eslintConfig; diff --git a/fix_persistence_data.js b/fix_persistence_data.js new file mode 100644 index 0000000..ff2138d --- /dev/null +++ b/fix_persistence_data.js @@ -0,0 +1,59 @@ +const { Client } = require('pg'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); +const root = '/root/miaojingAI'; +const c = new Client({ connectionString: process.env.LOCAL_DB_URL }); +function keyFromUrl(url) { + if (!url) return null; + const marker = '/api/local-storage/'; + const idx = String(url).indexOf(marker); + if (idx < 0) return null; + try { return decodeURIComponent(String(url).slice(idx + marker.length).split('?')[0]); } catch { return null; } +} +function publicUrl(key) { return `/api/local-storage/${key}`; } +function copyToGallery(url, type, id, field) { + const key = keyFromUrl(url); + if (!key) return url; + if (key.startsWith('gallery/')) return url; + const src = path.join(root, 'local-storage', key); + if (!fs.existsSync(src)) return url; + const ext = path.extname(key) || '.bin'; + const folder = field === 'thumbnail_url' ? 'gallery/thumbnails' : (type === 'text2video' || type === 'img2video' || type === 'video' ? 'gallery/videos' : 'gallery/images'); + const destDir = path.join(root, 'local-storage', folder); + fs.mkdirSync(destDir, { recursive: true }); + const destKey = `${folder}/${id}-${field}${ext}`; + const dest = path.join(root, 'local-storage', destKey); + if (!fs.existsSync(dest)) fs.copyFileSync(src, dest); + return publicUrl(destKey); +} +(async () => { + await c.connect(); + await c.query('BEGIN'); + try { + const adminRes = await c.query(`select id,email from profiles where role='admin' order by case when email='admin@example.com' then 0 else 1 end, created_at asc limit 1`); + if (!adminRes.rows.length) throw new Error('No admin profile found'); + const admin = adminRes.rows[0]; + const password = process.env.ADMIN_DEFAULT_PASSWORD || 'admin123'; + await c.query(`update auth.users set password_hash = crypt($1, gen_salt('bf')) where id=$2`, [password, admin.id]); + const worksFixed = await c.query(`update works set user_id=$1 where user_id is null or user_id not in (select id from profiles) returning id`, [admin.id]); + const creditDeleted = await c.query(`delete from credit_transactions where user_id not in (select id from profiles) returning id`); + const publicWorks = await c.query(`select id,type,result_url,thumbnail_url from works where is_public=true order by created_at asc`); + let copied = 0; + for (const w of publicWorks.rows) { + const nextResult = copyToGallery(w.result_url, w.type, w.id, 'result_url'); + const nextThumb = w.thumbnail_url ? copyToGallery(w.thumbnail_url, w.type, w.id, 'thumbnail_url') : null; + if (nextResult !== w.result_url || nextThumb !== w.thumbnail_url) { + await c.query(`update works set result_url=$1, thumbnail_url=$2, updated_at=now() where id=$3`, [nextResult, nextThumb, w.id]); + copied++; + } + } + await c.query('COMMIT'); + console.log(JSON.stringify({ admin: admin.email, passwordHashSet: true, worksFixed: worksFixed.rowCount, orphanCreditsDeleted: creditDeleted.rowCount, publicWorksCopiedToGallery: copied }, null, 2)); + } catch (e) { + await c.query('ROLLBACK'); + throw e; + } finally { + await c.end(); + } +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/inspect_before_fix.js b/inspect_before_fix.js new file mode 100644 index 0000000..19d4031 --- /dev/null +++ b/inspect_before_fix.js @@ -0,0 +1,12 @@ +const { Client } = require('pg'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); +const c = new Client({ connectionString: process.env.LOCAL_DB_URL }); +(async () => { + await c.connect(); + for (const [name, sql] of [ + ['works_public', `select is_public, count(*)::int from works group by is_public order by is_public`], + ['works_urls', `select id,user_id,is_public,type,result_url,thumbnail_url from works order by created_at desc`], + ['orphan_credit', `select id,user_id,amount,type,description from credit_transactions where user_id not in (select id from profiles)`] + ]) { const r=await c.query(sql); console.log('--- '+name); console.table(r.rows); } + await c.end(); +})().catch(e=>{console.error(e);process.exit(1)}); diff --git a/inspect_db.js b/inspect_db.js new file mode 100644 index 0000000..075378a --- /dev/null +++ b/inspect_db.js @@ -0,0 +1,19 @@ +const { Client } = require('pg'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); +const c = new Client({ connectionString: process.env.LOCAL_DB_URL }); +(async () => { + await c.connect(); + const queries = [ + ['profiles_count', 'select count(*)::int as n from profiles'], + ['profiles', 'select id,email,nickname,role,membership_tier,is_active,created_at from profiles order by created_at desc limit 20'], + ['jobs_count', 'select count(*)::int as n from generation_jobs'], + ['jobs', 'select id,user_id,type,status,created_at from generation_jobs order by created_at desc limit 20'], + ['jobs_join_profiles', `select j.id,j.user_id,p.email,p.nickname,p.role,j.type,j.status,j.created_at from generation_jobs j left join profiles p on p.id=j.user_id order by j.created_at desc limit 20`] + ]; + for (const [name, sql] of queries) { + const r = await c.query(sql); + console.log('--- ' + name); + console.table(r.rows); + } + await c.end(); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/inspect_gallery_model_owner_candidates.js b/inspect_gallery_model_owner_candidates.js new file mode 100644 index 0000000..df60259 --- /dev/null +++ b/inspect_gallery_model_owner_candidates.js @@ -0,0 +1,61 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const apiKeys = await client.query(` + SELECT id, user_id, provider, supplier_name, model_name, note, type, created_at + FROM user_api_keys + ORDER BY created_at DESC + LIMIT 200 + `); + + const modelCounts = await client.query(` + SELECT params->>'model' AS model, COUNT(*)::int AS count + FROM works + WHERE is_public = true + AND status = 'completed' + AND user_id = $1 + GROUP BY params->>'model' + ORDER BY count DESC + `, [SYSTEM_USER_ID]); + + const directMatches = await client.query(` + SELECT + w.params->>'model' AS work_model, + COUNT(*)::int AS work_count, + k.id AS api_key_id, + k.user_id, + p.email, + p.nickname, + k.provider, + k.supplier_name, + k.model_name, + k.note + FROM works w + JOIN user_api_keys k ON w.params->>'model' = CONCAT('custom:', k.id::text) + JOIN profiles p ON p.id = k.user_id + WHERE w.is_public = true + AND w.status = 'completed' + AND w.user_id = $1 + GROUP BY w.params->>'model', k.id, k.user_id, p.email, p.nickname, k.provider, k.supplier_name, k.model_name, k.note + ORDER BY work_count DESC + `, [SYSTEM_USER_ID]); + + console.log(JSON.stringify({ + userApiKeys: apiKeys.rows, + anonymousGalleryModelCounts: modelCounts.rows, + directMatches: directMatches.rows, + }, null, 2)); + } finally { + client.release(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/inspect_gallery_user_links.js b/inspect_gallery_user_links.js new file mode 100644 index 0000000..2c874d3 --- /dev/null +++ b/inspect_gallery_user_links.js @@ -0,0 +1,53 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const summary = await client.query(` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE w.user_id IS NULL)::int AS null_user_id, + COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile, + COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE w.is_public = true AND w.status = 'completed' + `); + + const samples = await client.query(` + SELECT + w.id, + w.user_id, + w.title, + LEFT(COALESCE(w.prompt, ''), 80) AS prompt_preview, + w.params, + w.created_at + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE w.is_public = true AND w.status = 'completed' AND p.id IS NULL + ORDER BY w.created_at DESC + LIMIT 20 + `); + + const profileSamples = await client.query(` + SELECT id, email, nickname, role, created_at + FROM profiles + ORDER BY created_at DESC + LIMIT 20 + `); + + console.log(JSON.stringify({ + summary: summary.rows[0], + missingSamples: samples.rows, + profileSamples: profileSamples.rows, + }, null, 2)); + } finally { + client.release(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/inspect_payload.js b/inspect_payload.js new file mode 100644 index 0000000..d40d33d --- /dev/null +++ b/inspect_payload.js @@ -0,0 +1,9 @@ +const { Client } = require('pg'); +require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); +const c = new Client({ connectionString: process.env.LOCAL_DB_URL }); +(async () => { + await c.connect(); + const r = await c.query(`select id, payload from generation_jobs order by created_at desc limit 3`); + for (const row of r.rows) { console.log('---', row.id); console.log(JSON.stringify(row.payload).slice(0, 1200)); } + await c.end(); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/inspect_recovery_candidates.js b/inspect_recovery_candidates.js new file mode 100644 index 0000000..55e1a5c --- /dev/null +++ b/inspect_recovery_candidates.js @@ -0,0 +1,111 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const byModelOwner = await client.query(` + SELECT + COALESCE(w.params->>'model', '') AS model, + COALESCE(p.email, '[missing-profile]') AS email, + COALESCE(p.nickname, '') AS nickname, + w.user_id, + COUNT(*)::int AS count, + COUNT(*) FILTER (WHERE w.is_public = true)::int AS public_count, + COUNT(*) FILTER (WHERE w.is_public = false)::int AS private_count, + MIN(w.created_at) AS first_at, + MAX(w.created_at) AS last_at + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE w.status = 'completed' + GROUP BY model, w.user_id, p.email, p.nickname + ORDER BY model, count DESC + `); + + const orphanModelTotals = await client.query(` + SELECT params->>'model' AS model, COUNT(*)::int AS count + FROM works + WHERE status = 'completed' + AND is_public = true + AND user_id = $1 + GROUP BY params->>'model' + ORDER BY count DESC + `, [SYSTEM_USER_ID]); + + const ownersByPrompt = await client.query(` + WITH orphan AS ( + SELECT id, prompt, created_at + FROM works + WHERE status = 'completed' + AND is_public = true + AND user_id = $1 + AND COALESCE(prompt, '') <> '' + ), + owned AS ( + SELECT w.id, w.user_id, p.email, p.nickname, w.prompt, w.created_at + FROM works w + JOIN profiles p ON p.id = w.user_id + WHERE w.status = 'completed' + AND w.user_id <> $1 + AND COALESCE(w.prompt, '') <> '' + ) + SELECT + orphan.id AS orphan_id, + owned.user_id, + owned.email, + owned.nickname, + COUNT(*)::int AS matches, + MIN(ABS(EXTRACT(EPOCH FROM (owned.created_at - orphan.created_at))))::int AS best_seconds_apart + FROM orphan + JOIN owned ON owned.prompt = orphan.prompt + GROUP BY orphan.id, owned.user_id, owned.email, owned.nickname + ORDER BY matches DESC, best_seconds_apart + LIMIT 120 + `, [SYSTEM_USER_ID]); + + const maybeAdminByDates = await client.query(` + SELECT + w.id, + w.created_at, + w.params->>'model' AS model, + LEFT(w.prompt, 100) AS prompt, + ( + SELECT json_agg(json_build_object( + 'user_id', nearby.user_id, + 'email', p.email, + 'nickname', p.nickname, + 'seconds_apart', ABS(EXTRACT(EPOCH FROM (nearby.created_at - w.created_at)))::int, + 'prompt', LEFT(nearby.prompt, 80) + ) ORDER BY ABS(EXTRACT(EPOCH FROM (nearby.created_at - w.created_at)))) + FROM works nearby + JOIN profiles p ON p.id = nearby.user_id + WHERE nearby.user_id <> $1 + AND nearby.status = 'completed' + AND nearby.created_at BETWEEN w.created_at - INTERVAL '1 hour' AND w.created_at + INTERVAL '1 hour' + LIMIT 8 + ) AS nearby_owned + FROM works w + WHERE w.status = 'completed' + AND w.is_public = true + AND w.user_id = $1 + ORDER BY w.created_at DESC + LIMIT 80 + `, [SYSTEM_USER_ID]); + + console.log(JSON.stringify({ + orphanModelTotals: orphanModelTotals.rows, + byModelOwner: byModelOwner.rows, + ownersByPrompt: ownersByPrompt.rows, + nearbyOwnedByTime: maybeAdminByDates.rows, + }, null, 2)); + } finally { + client.release(); + await pool.end(); + } +})().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/inspect_work_link_candidates.js b/inspect_work_link_candidates.js new file mode 100644 index 0000000..43da2c9 --- /dev/null +++ b/inspect_work_link_candidates.js @@ -0,0 +1,59 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + const duplicates = await client.query(` + SELECT + public.id AS public_id, + public.user_id AS public_user_id, + private.id AS private_id, + private.user_id AS private_user_id, + p.email, + p.nickname, + public.result_url AS public_url, + private.result_url AS private_url, + LEFT(COALESCE(public.prompt, ''), 80) AS prompt_preview + FROM works public + JOIN works private + ON private.id <> public.id + AND private.user_id <> '00000000-0000-0000-0000-000000000000' + AND ( + private.result_url = public.result_url + OR private.thumbnail_url = public.thumbnail_url + OR ( + COALESCE(private.prompt, '') = COALESCE(public.prompt, '') + AND private.created_at BETWEEN public.created_at - INTERVAL '10 minutes' AND public.created_at + INTERVAL '10 minutes' + ) + ) + JOIN profiles p ON p.id = private.user_id + WHERE public.is_public = true + AND public.status = 'completed' + AND public.user_id = '00000000-0000-0000-0000-000000000000' + LIMIT 50 + `); + + const paramKeys = await client.query(` + SELECT id, params + FROM works + WHERE is_public = true + AND status = 'completed' + AND user_id = '00000000-0000-0000-0000-000000000000' + LIMIT 10 + `); + + console.log(JSON.stringify({ + duplicateCandidateCount: duplicates.rowCount, + duplicateCandidates: duplicates.rows, + paramSamples: paramKeys.rows, + }, null, 2)); + } finally { + client.release(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/model-config-migration.sql b/model-config-migration.sql new file mode 100644 index 0000000..0347862 --- /dev/null +++ b/model-config-migration.sql @@ -0,0 +1,57 @@ +CREATE TABLE IF NOT EXISTS api_providers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(128) NOT NULL UNIQUE, + default_api_url TEXT, + default_model VARCHAR(255), + type VARCHAR(16) NOT NULL DEFAULT 'image', + website TEXT, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS model_recommendations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + model_name VARCHAR(255) NOT NULL, + display_name VARCHAR(255), + type VARCHAR(16) NOT NULL DEFAULT 'image', + provider_id UUID REFERENCES api_providers(id) ON DELETE SET NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS api_providers_active_sort_idx ON api_providers (is_active, sort_order); +CREATE INDEX IF NOT EXISTS model_recommendations_active_type_sort_idx ON model_recommendations (is_active, type, sort_order); +CREATE INDEX IF NOT EXISTS model_recommendations_provider_idx ON model_recommendations (provider_id); + +INSERT INTO api_providers (name, default_api_url, default_model, type, website, is_active, sort_order) +VALUES + ('硅基流动', 'https://api.siliconflow.cn/v1/images/generations', 'black-forest-labs/FLUX.1-schnell', 'image', 'https://cloud.siliconflow.cn', true, 10), + ('mozheAPI', 'https://openai.mozhevip.top', '', 'image', 'https://openai.mozhevip.top', true, 20), + ('OpenAI', 'https://api.openai.com/v1/images/generations', 'dall-e-3', 'image', NULL, true, 30), + ('Stability AI', 'https://api.stability.ai/v1/generation/stable-diffusion-xl/text-to-image', 'stable-diffusion-xl', 'image', NULL, true, 40), + ('Midjourney', '', 'midjourney-v6', 'image', NULL, true, 50), + ('Runway', 'https://api.runwayml.com/v1/image_to_video', 'gen-3-alpha', 'video', NULL, true, 60), + ('Pika', '', 'pika-1.0', 'video', NULL, true, 70), + ('Kling', '', 'kling-v1', 'video', NULL, true, 80), + ('DeepSeek', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 'text', NULL, true, 90), + ('OpenAI GPT', 'https://api.openai.com/v1/chat/completions', 'gpt-4o', 'text', NULL, true, 100), + ('自定义', '', '', 'image', NULL, true, 999) +ON CONFLICT (name) DO UPDATE SET + default_api_url = EXCLUDED.default_api_url, + default_model = EXCLUDED.default_model, + type = EXCLUDED.type, + website = EXCLUDED.website, + is_active = EXCLUDED.is_active, + sort_order = EXCLUDED.sort_order, + updated_at = NOW(); + +INSERT INTO model_recommendations (model_name, display_name, type, provider_id, is_active, sort_order) +SELECT 'gpt-image-2', 'gpt-image-2', 'image', NULL, true, 10 +WHERE NOT EXISTS ( + SELECT 1 FROM model_recommendations + WHERE model_name = 'gpt-image-2' AND type = 'image' AND provider_id IS NULL +); diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..2f2ed30 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,19 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // outputFileTracingRoot: path.resolve(__dirname, '../../'), // Uncomment and add 'import path from "path"' if needed + /* config options here */ + poweredByHeader: false, + allowedDevOrigins: ['*.dev.coze.site'], + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*', + pathname: '/**', + }, + ], + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..38a1f9b --- /dev/null +++ b/package.json @@ -0,0 +1,113 @@ +{ + "name": "projects", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "bash ./scripts/build.sh", + "build:with-install": "INSTALL_DEPS=1 bash ./scripts/build.sh", + "backup:create": "bash ./scripts/backup-create.sh", + "backup:list": "bash ./scripts/backup-list.sh", + "backup:restore": "bash ./scripts/backup-restore.sh", + "db:patch": "bash ./scripts/apply-database-patch.sh", + "dev": "bash ./scripts/dev.sh", + "preinstall": "npx only-allow pnpm", + "lint": "eslint", + "start": "bash ./scripts/start.sh", + "pm2:restart": "pm2 startOrReload ecosystem.config.cjs --update-env", + "pm2:save": "pm2 save", + "ts-check": "tsc -p tsconfig.json", + "check:boundaries": "bash ./scripts/check-boundaries.sh" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.958.0", + "@aws-sdk/lib-storage": "^3.958.0", + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "2.95.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "coze-coding-dev-sdk": "^0.7.21", + "date-fns": "^4.1.0", + "dotenv": "^17.2.3", + "drizzle-orm": "^0.45.2", + "drizzle-zod": "^0.8.3", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.468.0", + "next": "16.2.4", + "next-themes": "^0.4.6", + "pg": "^8.17.2", + "react": "19.2.3", + "react-day-picker": "^9.13.0", + "react-dom": "19.2.3", + "react-hook-form": "^7.70.0", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^4.2.0", + "remark-gfm": "^4.0.1", + "sharp": "^0.34.5", + "sonner": "^2.0.7", + "tailwind-merge": "^2.6.0", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "zod": "^4.3.5" + }, + "devDependencies": { + "@react-dev-inspector/babel-plugin": "^2.0.1", + "@react-dev-inspector/middleware": "^2.0.1", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/pg": "^8.16.0", + "@types/react": "^19", + "@types/react-dom": "^19", + "drizzle-kit": "^0.31.8", + "eslint": "^9", + "eslint-config-next": "16.2.4", + "only-allow": "^1.2.2", + "react-dev-inspector": "^2.0.1", + "recharts": "2.15.4", + "shadcn": "latest", + "tailwindcss": "^4", + "tsup": "^8.3.5", + "tsx": "^4.19.2", + "typescript": "^5" + }, + "packageManager": "pnpm@9.0.0", + "pnpm": { + "overrides": { + "@langchain/core": "1.1.44", + "langsmith": "0.6.0", + "fast-xml-parser": "5.7.0", + "postcss": "8.5.10", + "uuid": "14.0.0" + } + }, + "engines": { + "pnpm": ">=9.0.0" + } +} diff --git a/persistence_migration.sql b/persistence_migration.sql new file mode 100644 index 0000000..c0b33b8 --- /dev/null +++ b/persistence_migration.sql @@ -0,0 +1,4 @@ +ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128); +ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image'; +CREATE INDEX IF NOT EXISTS user_api_keys_user_active_idx ON user_api_keys (user_id, is_active); +CREATE INDEX IF NOT EXISTS works_user_result_url_idx ON works (user_id, result_url); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d9a03ee --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,13336 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@langchain/core': 1.1.44 + langsmith: 0.6.0 + fast-xml-parser: 5.7.0 + postcss: 8.5.10 + uuid: 14.0.0 + +importers: + + .: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.958.0 + version: 3.975.0 + '@aws-sdk/lib-storage': + specifier: ^3.958.0 + version: 3.975.0(@aws-sdk/client-s3@3.975.0) + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.71.1(react@19.2.3)) + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-aspect-ratio': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-avatar': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-context-menu': + specifier: ^2.2.16 + version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-hover-card': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menubar': + specifier: ^1.1.16 + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.14 + version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-progress': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slider': + specifier: ^1.3.6 + version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@supabase/supabase-js': + specifier: 2.95.3 + version: 2.95.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + coze-coding-dev-sdk: + specifier: ^0.7.21 + version: 0.7.21(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@types/pg@8.16.0)(pg@8.17.2) + drizzle-zod: + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@0.45.2(@types/pg@8.16.0)(pg@8.17.2))(zod@4.3.6) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.2.3) + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + lucide-react: + specifier: ^0.468.0 + version: 0.468.0(react@19.2.3) + next: + specifier: 16.2.4 + version: 16.2.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + pg: + specifier: ^8.17.2 + version: 8.17.2 + react: + specifier: 19.2.3 + version: 19.2.3 + react-day-picker: + specifier: ^9.13.0 + version: 9.13.0(react@19.2.3) + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + react-hook-form: + specifier: ^7.70.0 + version: 7.71.1(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.10)(react@19.2.3) + react-resizable-panels: + specifier: ^4.2.0 + version: 4.5.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + zod: + specifier: ^4.3.5 + version: 4.3.6 + devDependencies: + '@react-dev-inspector/babel-plugin': + specifier: ^2.0.1 + version: 2.0.1 + '@react-dev-inspector/middleware': + specifier: ^2.0.1 + version: 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/node': + specifier: ^20 + version: 20.19.30 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 + '@types/react': + specifier: ^19 + version: 19.2.10 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.10) + drizzle-kit: + specifier: ^0.31.8 + version: 0.31.8 + eslint: + specifier: ^9 + version: 9.39.2(jiti@2.6.1) + eslint-config-next: + specifier: 16.2.4 + version: 16.2.4(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + only-allow: + specifier: ^1.2.2 + version: 1.2.2 + react-dev-inspector: + specifier: ^2.0.1 + version: 2.0.1(@types/react@19.2.10)(eslint@9.39.2(jiti@2.6.1))(react@19.2.3)(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + recharts: + specifier: 2.15.4 + version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + shadcn: + specifier: latest + version: 3.7.0(@cfworker/json-schema@4.1.1)(@types/node@20.19.30)(hono@4.11.7)(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.1.18 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.2) + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@antfu/ni@25.0.0': + resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} + hasBin: true + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.975.0': + resolution: {integrity: sha512-aF1M/iMD29BPcpxjqoym0YFa4WR9Xie1/IhVumwOGH6TB45DaqYO7vLwantDBcYNRn/cZH6DFHksO7RmwTFBhw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sso@3.975.0': + resolution: {integrity: sha512-HpgJuleH7P6uILxzJKQOmlHdwaCY+xYC6VgRDzlwVEqU/HXjo4m2gOAyjUbpXlBOCWfGgMUzfBlNJ9z3MboqEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.972.0': + resolution: {integrity: sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.3': + resolution: {integrity: sha512-ZbM2Xy8ytAcfnNpkBltr6Qdw36W/4NW5nZdZieCuTfacoBFpi/NYiwb8U05KNJvLKeZnrV9Vi696i+r2DQFORg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.0': + resolution: {integrity: sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.2': + resolution: {integrity: sha512-wzH1EdrZsytG1xN9UHaK12J9+kfrnd2+c8y0LVoS4O4laEjPoie1qVK3k8/rZe7KOtvULzyMnO3FT4Krr9Z0Dg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.3': + resolution: {integrity: sha512-IbBGWhaxiEl64fznwh5PDEB0N7YJEAvK5b6nRtPVUKdKAHlOPgo6B9XB8mqWDs8Ct0oF/E34ZLiq2U0L5xDkrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.2': + resolution: {integrity: sha512-Jrb8sLm6k8+L7520irBrvCtdLxNtrG7arIxe9TCeMJt/HxqMGJdbIjw8wILzkEHLMIi4MecF2FbXCln7OT1Tag==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.2': + resolution: {integrity: sha512-mlaw2aiI3DrimW85ZMn3g7qrtHueidS58IGytZ+mbFpsYLK5wMjCAKZQtt7VatLMtSBG/dn/EY4njbnYXIDKeQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.2': + resolution: {integrity: sha512-Lz1J5IZdTjLYTVIcDP5DVDgi1xlgsF3p1cnvmbfKbjCRhQpftN2e2J4NFfRRvPD54W9+bZ8l5VipPXtTYK7aEg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.2': + resolution: {integrity: sha512-NLKLTT7jnUe9GpQAVkPTJO+cs2FjlQDt5fArIYS7h/Iw/CvamzgGYGFRVD2SE05nOHCMwafUSi42If8esGFV+g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.2': + resolution: {integrity: sha512-YpwDn8g3gCGUl61cCV0sRxP2pFIwg+ZsMfWQ/GalSyjXtRkctCMFA+u0yPb/Q4uTfNEiya1Y4nm0C5rIHyPW5Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.2': + resolution: {integrity: sha512-x9DAiN9Qz+NjJ99ltDiVQ8d511M/tuF/9MFbe2jUgo7HZhD6+x4S3iT1YcP07ndwDUjmzKGmeOEgE24k4qvfdg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.975.0': + resolution: {integrity: sha512-F6vrnZ3F7oqr3oONCIpx+uZDTwXWfh8sBoNNJollDn5pIn7TI+R+7WxVIXAMq/JWLXE6N8T3M6ogWk4Y4JWPPw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-s3': 3.975.0 + + '@aws-sdk/middleware-bucket-endpoint@3.972.1': + resolution: {integrity: sha512-YVvoitBdE8WOpHqIXvv49efT73F4bJ99XH2bi3Dn3mx7WngI4RwHwn/zF5i0q1Wdi5frGSCNF3vuh+pY817//w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.1': + resolution: {integrity: sha512-6lfl2/J/kutzw/RLu1kjbahsz4vrGPysrdxWaw8fkjLYG+6M6AswocIAZFS/LgAVi/IWRwPTx9YC0/NH2wDrSw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.972.1': + resolution: {integrity: sha512-kjVVREpqeUkYQsXr78AcsJbEUlxGH7+H6yS7zkjrnu6HyEVxbdSndkKX6VpKneFOihjCAhIXlk4wf3butDHkNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.2': + resolution: {integrity: sha512-42hZ8jEXT2uR6YybCzNq9OomqHPw43YIfRfz17biZjMQA4jKSQUaHIl6VvqO2Ddl5904pXg2Yd/ku78S0Ikgog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.1': + resolution: {integrity: sha512-YisPaCbvBk9gY5aUI8jDMDKXsLZ9Fet0WYj1MviK8tZYMgxBIYHM6l3O/OHaAIujojZvamd9F3haYYYWp5/V3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.2': + resolution: {integrity: sha512-iUzdXKOgi4JVDDEG/VvoNw50FryRCEm0qAudw12DcZoiNJWl0rN6SYVLcL1xwugMfQncCXieK5UBlG6mhH7iYA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.2': + resolution: {integrity: sha512-/mzlyzJDtngNFd/rAYvqx29a2d0VuiYKN84Y/Mu9mGw7cfMOCyRK+896tb9wV6MoPRHUX7IXuKCIL8nzz2Pz5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.0': + resolution: {integrity: sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.2': + resolution: {integrity: sha512-5f9x9/G+StE8+7wd9EVDF3d+J74xK+WBA3FhZwLSkf3pHFGLKzlmUfxJJE1kkXkbj/j/H+Dh3zL/hrtQE9hNsg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.1': + resolution: {integrity: sha512-fLtRTPd/MxJT2drJKft2GVGKm35PiNEeQ1Dvz1vc/WhhgAteYrp4f1SfSgjgLaYWGMExESJL4bt8Dxqp6tVsog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.3': + resolution: {integrity: sha512-zq6aTiO/BiAIOA8EH8nB+wYvvnZ14Md9Gomm5DDhParshVEVglAyNPO5ADK4ZXFQbftIoO+Vgcvf4gewW/+iYQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.975.0': + resolution: {integrity: sha512-OkeFHPlQj2c/Y5bQGkX14pxhDWUGUFt3LRHhjcDKsSCw6lrxKcxN3WFZN0qbJwKNydP+knL5nxvfgKiCLpTLRA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.2': + resolution: {integrity: sha512-/7vRBsfmiOlg2X67EdKrzzQGw5/SbkXb7ALHQmlQLkZh8qNgvS2G2dDC6NtF3hzFlpP3j2k+KIEtql/6VrI6JA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.972.0': + resolution: {integrity: sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.975.0': + resolution: {integrity: sha512-AWQt64hkVbDQ+CmM09wnvSk2mVyH4iRROkmYkr3/lmUtFNbE2L/fnw26sckZnUcFCsHPqbkQrcsZAnTcBLbH4w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.972.0': + resolution: {integrity: sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.1': + resolution: {integrity: sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.0': + resolution: {integrity: sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.1': + resolution: {integrity: sha512-XnNit6H9PPHhqUXW/usjX6JeJ6Pm8ZNqivTjmNjgWHeOfVpblUc/MTic02UmCNR0jJLPjQ3mBKiMen0tnkNQjQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.972.0': + resolution: {integrity: sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.4': + resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.2': + resolution: {integrity: sha512-gz76bUyebPZRxIsBHJUd/v+yiyFzm9adHbr8NykP2nm+z/rFyvQneOHajrUejtmnc5tTBeaDPL4X25TnagRk4A==} + + '@aws-sdk/util-user-agent-node@3.972.2': + resolution: {integrity: sha512-vnxOc4C6AR7hVbwyFo1YuH0GB6dgJlWt8nIOOJpnzJAWJPkUMPJ9Zv2lnKsSU7TTZbhP2hEO8OZ4PYH59XFv8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.0': + resolution: {integrity: sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.2': + resolution: {integrity: sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.20.5': + resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@dotenvx/dotenvx@1.52.0': + resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} + hasBin: true + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@langchain/core@1.1.44': + resolution: {integrity: sha512-RePW1IjGCHr9ua2vcby3aE8mOOz3EnwDZxMEGbNDT91kf14eqkJqxDXvaZFviGdcN9DTrxM5RPQNAHmwSm4tbg==} + engines: {node: '>=20'} + + '@langchain/openai@1.4.4': + resolution: {integrity: sha512-mRr/X5rvlwPj6cSXPxbL+CtOqYANO1/+CQ3Z+5t48kWnrlgPYOazmA+UAWvqQOuwJ6LaYn3SFrt43rR4lte/Ow==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': 1.1.44 + + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@16.2.4': + resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} + + '@next/eslint-plugin-next@16.2.4': + resolution: {integrity: sha512-tOX826JJ96gYK/go18sPUgMq9FK1tqxBFfUCEufJb5XIkWFFmpgU7mahJANKGkHs7F41ir3tReJ3Lv5La0RvhA==} + + '@next/swc-darwin-arm64@16.2.4': + resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.4': + resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.4': + resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.2.4': + resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.2.4': + resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.2.4': + resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.2.4': + resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.2.4': + resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.8': + resolution: {integrity: sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.11': + resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.8': + resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@react-dev-inspector/babel-plugin@2.0.1': + resolution: {integrity: sha512-V2MzN9dj3uZu6NvAjSxXwa3+FOciVIuwAUwPLpO6ji5xpUyx8E6UiEng1QqzttdpacKHFKtkNYjtQAE+Lsqa5A==} + engines: {node: '>=12.0.0'} + + '@react-dev-inspector/middleware@2.0.1': + resolution: {integrity: sha512-qDMtBzAxNNAX01jjU1THZVuNiVB7J1Hjk42k8iLSSwfinc3hk667iqgdzeq1Za1a0V2bF5Ev6D4+nkZ+E1YUrQ==} + engines: {node: '>=12.0.0'} + + '@react-dev-inspector/umi3-plugin@2.0.1': + resolution: {integrity: sha512-lRw65yKQdI/1BwrRXWJEHDJel4DWboOartGmR3S5xiTF+EiOLjmndxdA5LoVSdqbcggdtq5SWcsoZqI0TkhH7Q==} + engines: {node: '>=12.0.0'} + + '@react-dev-inspector/umi4-plugin@2.0.1': + resolution: {integrity: sha512-vTefsJVAZsgpuO9IZ1ZFIoyryVUU+hjV8OPD8DfDU+po5LjVXc5Uncn+MkFOsT24AMpNdDvCnTRYiuSkFn8EsA==} + engines: {node: '>=12.0.0'} + + '@react-dev-inspector/vite-plugin@2.0.1': + resolution: {integrity: sha512-J1eI7cIm2IXE6EwhHR1OyoefvobUJEn/vJWEBwOM5uW4JkkLwuVoV9vk++XJyAmKUNQ87gdWZvSWrI2LjfrSug==} + engines: {node: '>=12.0.0'} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.22.0': + resolution: {integrity: sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.8': + resolution: {integrity: sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.8': + resolution: {integrity: sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.8': + resolution: {integrity: sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.8': + resolution: {integrity: sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.8': + resolution: {integrity: sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.9': + resolution: {integrity: sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.8': + resolution: {integrity: sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.8': + resolution: {integrity: sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.12': + resolution: {integrity: sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.29': + resolution: {integrity: sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.8': + resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.11.1': + resolution: {integrity: sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.28': + resolution: {integrity: sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.31': + resolution: {integrity: sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.10': + resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.8': + resolution: {integrity: sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@supabase/auth-js@2.95.3': + resolution: {integrity: sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.95.3': + resolution: {integrity: sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig==} + engines: {node: '>=20.0.0'} + + '@supabase/postgrest-js@2.95.3': + resolution: {integrity: sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.95.3': + resolution: {integrity: sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw==} + engines: {node: '>=20.0.0'} + + '@supabase/storage-js@2.95.3': + resolution: {integrity: sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.95.3': + resolution: {integrity: sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==} + engines: {node: '>=20.0.0'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/node@20.19.39': + resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + + '@types/phoenix@1.6.7': + resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-reconciler@0.33.0': + resolution: {integrity: sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.10': + resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axios@1.15.2: + resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.21: + resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==} + engines: {node: '>=6.0.0'} + hasBin: true + + baseline-browser-mapping@2.9.18: + resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@6.0.0: + resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} + engines: {node: '>=8'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + coze-coding-dev-sdk@0.7.21: + resolution: {integrity: sha512-yO+vhA4+4ksF6KzPqISFob9CaEC9SrXAzkwh6nVot42kVoKTaK170lKhs3W2tkxVW6ejA+bi71bDhae9wWgNog==} + engines: {node: '>=18.0.0'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + detect-port-alt@1.1.6: + resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} + engines: {node: '>= 4.2.1'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + drizzle-kit@0.31.8: + resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} + hasBin: true + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eciesjs@0.4.17: + resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.279: + resolution: {integrity: sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==} + + electron-to-chromium@1.5.343: + resolution: {integrity: sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==} + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-next@16.2.4: + resolution: {integrity: sha512-A6ekXYFj/YQxBPMl45g3e+U8zJo+X2+ZQwcz34pPKjpc/3S4roBA2Rd9xWB4FKuSxhofo1/95WjzmUY+wHrOhg==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + + fast-xml-parser@5.7.0: + resolution: {integrity: sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filesize@8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fork-ts-checker-webpack-plugin@6.5.3: + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + engines: {node: '>=16.9.0'} + + hotkeys-js@3.13.15: + resolution: {integrity: sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-root@2.1.0: + resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} + engines: {node: '>=6'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + langsmith@0.6.0: + resolution: {integrity: sha512-GGaj5IMRfLv2HXXFzGk9diISMYLTpSTh6fzCZGKxWYW/NqEztIFtnXLq6G/RVhzFRmCykLap1fuC67LVKoQLcg==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + + loader-utils@3.3.1: + resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} + engines: {node: '>= 12.13.0'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.468.0: + resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.7: + resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@16.2.4: + resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + only-allow@1.2.2: + resolution: {integrity: sha512-uxyNYDsCh5YIJ780G7hC5OHjVUr9reHsbZNMM80L9tZlTpb3hUzb36KXgW4ZUGtJKQnGA3xegmWg1BxhWV0jJA==} + hasBin: true + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.10.1: + resolution: {integrity: sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.17.2: + resolution: {integrity: sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: 8.5.10 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-day-picker@9.13.0: + resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dev-inspector@2.0.1: + resolution: {integrity: sha512-b8PAmbwGFrWcxeaX8wYveqO+VTwTXGJaz/yl9RO31LK1zeLKJVlkkbeLExLnJ6IvhXY1TwL8Q4+gR2GKJ8BI6Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: '>=16.8.0' + + react-dev-utils@12.0.1: + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-error-overlay@6.1.0: + resolution: {integrity: sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==} + + react-hook-form@7.71.1: + resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@4.5.2: + resolution: {integrity: sha512-PJyyR41poi1O1MvvQzDVtEBRq1x7B/9jB6yoFbm67pm8AvPUUwhljFtxfhaYy8klsmkQ6AvxZgDxXTkDl4vy4Q==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-utils@2.7.0: + resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} + engines: {node: '>= 8.9.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@3.7.0: + resolution: {integrity: sha512-zOXNAIFclguSYmmoibyXyKiYA6qjEJtXDSvloAMziSREW9Q0R/dLqBUYdb81lOejmZkDYuZApGabbMLH7G8qvQ==} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: 8.5.10 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.4.2: + resolution: {integrity: sha512-FLEenlVYf7Zcd34ISMLo3ZzRE1gRjY1nMDTp+bQRBiPsaKyIW8K3Zr99ioHDUgA9OGuGGJPyYpNcffGmBhJfGg==} + engines: {node: '>=20'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.54.0: + resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + engines: {node: '>=10.13.0'} + + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@antfu/ni@25.0.0': + dependencies: + ansis: 4.2.0 + fzf: 0.5.2 + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.975.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.3 + '@aws-sdk/credential-provider-node': 3.972.2 + '@aws-sdk/middleware-bucket-endpoint': 3.972.1 + '@aws-sdk/middleware-expect-continue': 3.972.1 + '@aws-sdk/middleware-flexible-checksums': 3.972.1 + '@aws-sdk/middleware-host-header': 3.972.2 + '@aws-sdk/middleware-location-constraint': 3.972.1 + '@aws-sdk/middleware-logger': 3.972.2 + '@aws-sdk/middleware-recursion-detection': 3.972.2 + '@aws-sdk/middleware-sdk-s3': 3.972.2 + '@aws-sdk/middleware-ssec': 3.972.1 + '@aws-sdk/middleware-user-agent': 3.972.3 + '@aws-sdk/region-config-resolver': 3.972.2 + '@aws-sdk/signature-v4-multi-region': 3.972.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.2 + '@aws-sdk/util-user-agent-node': 3.972.2 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.0 + '@smithy/eventstream-serde-browser': 4.2.8 + '@smithy/eventstream-serde-config-resolver': 4.3.8 + '@smithy/eventstream-serde-node': 4.2.8 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-blob-browser': 4.2.9 + '@smithy/hash-node': 4.2.8 + '@smithy/hash-stream-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/md5-js': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.12 + '@smithy/middleware-retry': 4.4.29 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.28 + '@smithy/util-defaults-mode-node': 4.2.31 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.8 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.975.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.3 + '@aws-sdk/middleware-host-header': 3.972.2 + '@aws-sdk/middleware-logger': 3.972.2 + '@aws-sdk/middleware-recursion-detection': 3.972.2 + '@aws-sdk/middleware-user-agent': 3.972.3 + '@aws-sdk/region-config-resolver': 3.972.2 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.2 + '@aws-sdk/util-user-agent-node': 3.972.2 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.12 + '@smithy/middleware-retry': 4.4.29 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.28 + '@smithy/util-defaults-mode-node': 4.2.31 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.972.0': + dependencies: + '@aws-sdk/types': 3.972.0 + '@aws-sdk/xml-builder': 3.972.0 + '@smithy/core': 3.22.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.973.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/xml-builder': 3.972.2 + '@smithy/core': 3.22.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.3': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/types': 3.973.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/credential-provider-env': 3.972.2 + '@aws-sdk/credential-provider-http': 3.972.3 + '@aws-sdk/credential-provider-login': 3.972.2 + '@aws-sdk/credential-provider-process': 3.972.2 + '@aws-sdk/credential-provider-sso': 3.972.2 + '@aws-sdk/credential-provider-web-identity': 3.972.2 + '@aws-sdk/nested-clients': 3.975.0 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/nested-clients': 3.975.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.2': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.2 + '@aws-sdk/credential-provider-http': 3.972.3 + '@aws-sdk/credential-provider-ini': 3.972.2 + '@aws-sdk/credential-provider-process': 3.972.2 + '@aws-sdk/credential-provider-sso': 3.972.2 + '@aws-sdk/credential-provider-web-identity': 3.972.2 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.2': + dependencies: + '@aws-sdk/client-sso': 3.975.0 + '@aws-sdk/core': 3.973.3 + '@aws-sdk/token-providers': 3.975.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/nested-clients': 3.975.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/lib-storage@3.975.0(@aws-sdk/client-s3@3.975.0)': + dependencies: + '@aws-sdk/client-s3': 3.975.0 + '@smithy/abort-controller': 4.2.8 + '@smithy/middleware-endpoint': 4.4.12 + '@smithy/smithy-client': 4.11.1 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.972.1': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.3 + '@aws-sdk/crc64-nvme': 3.972.0 + '@aws-sdk/types': 3.973.1 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.2': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.2': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.2': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.0': + dependencies: + '@aws-sdk/core': 3.972.0 + '@aws-sdk/types': 3.972.0 + '@aws-sdk/util-arn-parser': 3.972.0 + '@smithy/core': 3.22.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.2': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.1 + '@smithy/core': 3.22.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.3': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.972.0 + '@smithy/core': 3.22.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.975.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.3 + '@aws-sdk/middleware-host-header': 3.972.2 + '@aws-sdk/middleware-logger': 3.972.2 + '@aws-sdk/middleware-recursion-detection': 3.972.2 + '@aws-sdk/middleware-user-agent': 3.972.3 + '@aws-sdk/region-config-resolver': 3.972.2 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.2 + '@aws-sdk/util-user-agent-node': 3.972.2 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.12 + '@smithy/middleware-retry': 4.4.29 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.28 + '@smithy/util-defaults-mode-node': 4.2.31 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.2': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.972.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.0 + '@aws-sdk/types': 3.972.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.975.0': + dependencies: + '@aws-sdk/core': 3.973.3 + '@aws-sdk/nested-clients': 3.975.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.1': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.1': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.972.0': + dependencies: + '@aws-sdk/types': 3.972.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.4': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.2': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.972.2': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.7.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.2': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.7.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.3': {} + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.6 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.6) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.6 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.6 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.6) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.20.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + to-fast-properties: 2.0.0 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cfworker/json-schema@4.1.1': {} + + '@date-fns/tz@1.4.1': {} + + '@dotenvx/dotenvx@1.52.0': + dependencies: + commander: 11.1.0 + dotenv: 17.2.3 + eciesjs: 0.4.17 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + + '@drizzle-team/brocli@0.10.2': {} + + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.10': {} + + '@hono/node-server@1.19.9(hono@4.11.7)': + dependencies: + hono: 4.11.7 + + '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.3))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.71.1(react@19.2.3) + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@20.19.30)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.30) + '@inquirer/type': 3.0.10(@types/node@20.19.30) + optionalDependencies: + '@types/node': 20.19.30 + + '@inquirer/core@10.3.2(@types/node@20.19.30)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.30) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.30 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@20.19.30)': + optionalDependencies: + '@types/node': 20.19.30 + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@langchain/core@1.1.44(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0)': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.6.0(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + mustache: 4.2.0 + p-queue: 6.6.2 + zod: 4.3.6 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + + '@langchain/openai@1.4.4(@langchain/core@1.1.44(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0))(ws@8.19.0)': + dependencies: + '@langchain/core': 1.1.44(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + js-tiktoken: 1.0.21 + openai: 6.34.0(ws@8.19.0)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - ws + + '@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - hono + - supports-color + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.2.4': {} + + '@next/eslint-plugin-next@16.2.4': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.2.4': + optional: true + + '@next/swc-darwin-x64@16.2.4': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.4': + optional: true + + '@next/swc-linux-arm64-musl@16.2.4': + optional: true + + '@next/swc-linux-x64-gnu@16.2.4': + optional: true + + '@next/swc-linux-x64-musl@16.2.4': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.4': + optional: true + + '@next/swc-win32-x64-msvc@16.2.4': + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodable/entities@2.1.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-aspect-ratio@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-context@1.1.3(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/rect@1.1.1': {} + + '@react-dev-inspector/babel-plugin@2.0.1': + dependencies: + '@babel/core': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.20.5 + transitivePeerDependencies: + - supports-color + + '@react-dev-inspector/middleware@2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3))': + dependencies: + react-dev-utils: 12.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + - vue-template-compiler + - webpack + + '@react-dev-inspector/umi3-plugin@2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3))': + dependencies: + '@react-dev-inspector/babel-plugin': 2.0.1 + '@react-dev-inspector/middleware': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + - vue-template-compiler + - webpack + + '@react-dev-inspector/umi4-plugin@2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3))': + dependencies: + '@react-dev-inspector/babel-plugin': 2.0.1 + '@react-dev-inspector/middleware': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + - vue-template-compiler + - webpack + + '@react-dev-inspector/vite-plugin@2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3))': + dependencies: + '@react-dev-inspector/middleware': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + - vue-template-compiler + - webpack + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@smithy/abort-controller@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.1': + dependencies: + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.6': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/core@3.22.0': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.8': + dependencies: + '@smithy/eventstream-codec': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.9': + dependencies: + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.12': + dependencies: + '@smithy/core': 3.22.0 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.29': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.8': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.8': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + + '@smithy/shared-ini-file-loader@4.4.3': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.8': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.11.1': + dependencies: + '@smithy/core': 3.22.0 + '@smithy/middleware-endpoint': 4.4.12 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 + tslib: 2.8.1 + + '@smithy/types@4.12.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.8': + dependencies: + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.28': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.31': + dependencies: + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.8': + dependencies: + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.10': + dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.8': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@supabase/auth-js@2.95.3': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.95.3': + dependencies: + tslib: 2.8.1 + + '@supabase/postgrest-js@2.95.3': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.95.3': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.95.3': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.95.3': + dependencies: + '@supabase/auth-js': 2.95.3 + '@supabase/functions-js': 2.95.3 + '@supabase/postgrest-js': 2.95.3 + '@supabase/realtime-js': 2.95.3 + '@supabase/storage-js': 2.95.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.10 + tailwindcss: 4.1.18 + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.1.1 + path-browserify: 1.0.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/node@20.19.39': + dependencies: + undici-types: 6.21.0 + + '@types/parse-json@4.0.2': {} + + '@types/pg@8.16.0': + dependencies: + '@types/node': 20.19.30 + pg-protocol: 1.11.0 + pg-types: 2.2.0 + + '@types/phoenix@1.6.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.10)': + dependencies: + '@types/react': 19.2.10 + + '@types/react-reconciler@0.33.0(@types/react@19.2.10)': + dependencies: + '@types/react': 19.2.10 + + '@types/react@19.2.10': + dependencies: + csstype: 3.2.3 + + '@types/statuses@2.0.6': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/validate-npm-package-name@4.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.30 + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.54.0': {} + + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + acorn@8.16.0: {} + + address@1.2.2: {} + + agent-base@7.1.4: {} + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansis@4.2.0: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axios@1.15.2: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.21: {} + + baseline-browser-mapping@2.9.18: {} + + binary-extensions@2.3.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bowser@2.13.1: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.18 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.279 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.21 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.343 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-from@1.1.2: {} + + buffer@5.6.0: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001766: {} + + caniuse-lite@1.0.30001790: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + commander@14.0.2: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@6.0.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + coze-coding-dev-sdk@0.7.21(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0): + dependencies: + '@langchain/core': 1.1.44(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + '@langchain/openai': 1.4.4(@langchain/core@1.1.44(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0))(ws@8.19.0) + '@supabase/supabase-js': 2.95.3 + axios: 1.15.2 + pg: 8.17.2 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - bufferutil + - debug + - openai + - pg-native + - utf-8-validate + - ws + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + damerau-levenshtein@1.0.8: {} + + data-uri-to-buffer@4.0.1: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js-light@2.5.1: {} + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dedent@1.7.1: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + detect-port-alt@1.1.6: + dependencies: + address: 1.2.2 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + dotenv@17.2.3: {} + + drizzle-kit@0.31.8: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.45.2(@types/pg@8.16.0)(pg@8.17.2): + optionalDependencies: + '@types/pg': 8.16.0 + pg: 8.17.2 + + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@types/pg@8.16.0)(pg@8.17.2))(zod@4.3.6): + dependencies: + drizzle-orm: 0.45.2(@types/pg@8.16.0)(pg@8.17.2) + zod: 4.3.6 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eciesjs@0.4.17: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.279: {} + + electron-to-chromium@1.5.343: {} + + embla-carousel-react@8.6.0(react@19.2.3): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.3 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + 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 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-next@16.2.4(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.2.4 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.3 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.2(jiti@2.6.1) + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + events@3.3.0: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.4.0: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.7.0: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filesize@8.0.7: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.1 + rollup: 4.59.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.16.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fork-ts-checker-webpack-plugin@6.5.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)): + dependencies: + '@babel/code-frame': 7.28.6 + '@types/json-schema': 7.0.15 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 6.0.0 + deepmerge: 4.3.1 + fs-extra: 9.1.0 + glob: 7.2.3 + memfs: 3.5.3 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.7.3 + tapable: 1.1.3 + typescript: 5.9.3 + webpack: 5.104.1(esbuild@0.27.3) + optionalDependencies: + eslint: 9.39.2(jiti@2.6.1) + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuzzysort@3.1.0: {} + + fzf@0.5.2: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.12.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hono@4.11.7: {} + + hotkeys-js@3.13.15: {} + + html-url-attributes@3.0.1: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + iceberg-js@0.8.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immer@9.0.21: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.2.7: {} + + input-otp@1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-regexp@3.1.0: {} + + is-root@2.1.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isexe@3.1.1: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 20.19.39 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@2.6.1: {} + + jose@6.1.3: {} + + joycon@3.1.1: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + langsmith@0.6.0(openai@6.34.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0): + dependencies: + p-queue: 6.6.2 + optionalDependencies: + openai: 6.34.0(ws@8.19.0)(zod@4.3.6) + ws: 8.19.0 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + loader-runner@4.3.1: {} + + loader-utils@3.3.1: {} + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.18.1: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.468.0(react@19.2.3): + dependencies: + react: 19.2.3 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.0.0: {} + + ms@2.1.3: {} + + msw@2.12.7(@types/node@20.19.30)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@20.19.30) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.2 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mustache@4.2.0: {} + + mute-stream@2.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + next@16.2.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.2.4 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.21 + caniuse-lite: 1.0.30001790 + postcss: 8.5.10 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.4 + '@next/swc-darwin-x64': 16.2.4 + '@next/swc-linux-arm64-gnu': 16.2.4 + '@next/swc-linux-arm64-musl': 16.2.4 + '@next/swc-linux-x64-gnu': 16.2.4 + '@next/swc-linux-x64-musl': 16.2.4 + '@next/swc-win32-arm64-msvc': 16.2.4 + '@next/swc-win32-x64-msvc': 16.2.4 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.27: {} + + node-releases@2.0.38: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-treeify@1.1.33: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + only-allow@1.2.2: + dependencies: + which-pm-runs: 1.1.0 + + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openai@6.34.0(ws@8.19.0)(zod@4.3.6): + optionalDependencies: + ws: 8.19.0 + zod: 4.3.6 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-try@2.2.0: {} + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.28.6 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-expression-matcher@1.5.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.3.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.10.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.11.0(pg@8.17.2): + dependencies: + pg: 8.17.2 + + pg-protocol@1.11.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.17.2: + dependencies: + pg-connection-string: 2.10.1 + pg-pool: 3.11.0(pg@8.17.2) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkce-challenge@5.0.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.2): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.10 + tsx: 4.21.0 + yaml: 1.10.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@7.1.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-day-picker@9.13.0(react@19.2.3): + dependencies: + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.3 + + react-dev-inspector@2.0.1(@types/react@19.2.10)(eslint@9.39.2(jiti@2.6.1))(react@19.2.3)(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)): + dependencies: + '@react-dev-inspector/babel-plugin': 2.0.1 + '@react-dev-inspector/middleware': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + '@react-dev-inspector/umi3-plugin': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + '@react-dev-inspector/umi4-plugin': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + '@react-dev-inspector/vite-plugin': 2.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + '@types/react-reconciler': 0.33.0(@types/react@19.2.10) + hotkeys-js: 3.13.15 + picocolors: 1.0.0 + react: 19.2.3 + react-dev-utils: 12.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + transitivePeerDependencies: + - '@types/react' + - eslint + - supports-color + - typescript + - vue-template-compiler + - webpack + + react-dev-utils@12.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)): + dependencies: + '@babel/code-frame': 7.28.6 + address: 1.2.2 + browserslist: 4.28.1 + chalk: 4.1.2 + cross-spawn: 7.0.6 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.3)) + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.21 + is-root: 2.1.0 + loader-utils: 3.3.1 + open: 8.4.2 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.1.0 + recursive-readdir: 2.2.3 + shell-quote: 1.8.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + webpack: 5.104.1(esbuild@0.27.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-error-overlay@6.1.0: {} + + react-hook-form@7.71.1(react@19.2.3): + dependencies: + react: 19.2.3 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-markdown@10.1.0(@types/react@19.2.10)(react@19.2.3): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.10 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-remove-scroll-bar@2.3.8(@types/react@19.2.10)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.10)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.10 + + react-remove-scroll@2.7.2(@types/react@19.2.10)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.10)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.10)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.10)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + + react-resizable-panels@4.5.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + react-smooth@4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + + react-style-singleton@2.2.3(@types/react@19.2.10)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.10 + + react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + react@19.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.18.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + recursive-readdir@2.2.3: + dependencies: + minimatch: 3.1.2 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.7.0: {} + + reusify@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + 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.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + schema-utils@2.7.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) + + semver@6.3.1: {} + + semver@7.7.3: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.2.0: {} + + shadcn@3.7.0(@cfworker/json-schema@4.1.1)(@types/node@20.19.30)(hono@4.11.7)(typescript@5.9.3): + dependencies: + '@antfu/ni': 25.0.0 + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.28.6) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) + '@dotenvx/dotenvx': 1.52.0 + '@modelcontextprotocol/sdk': 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.1 + commander: 14.0.2 + cosmiconfig: 9.0.0(typescript@5.9.3) + dedent: 1.7.1 + deepmerge: 4.3.1 + diff: 8.0.3 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.3 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + msw: 2.12.7(@types/node@20.19.30)(typescript@5.9.3) + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.10 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/node' + - babel-plugin-macros + - hono + - supports-color + - typescript + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + split2@4.2.0: {} + + stable-hash@0.0.5: {} + + statuses@2.0.2: {} + + stdin-discarder@0.2.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + strnum@2.2.3: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optionalDependencies: + '@babel/core': 7.28.6 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tailwind-merge@2.6.0: {} + + tailwindcss@4.1.18: {} + + tapable@1.1.3: {} + + tapable@2.3.0: {} + + tapable@2.3.3: {} + + terser-webpack-plugin@5.4.0(esbuild@0.27.3)(webpack@5.104.1(esbuild@0.27.3)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.104.1(esbuild@0.27.3) + optionalDependencies: + esbuild: 0.27.3 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.2) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.10 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.4.2: + dependencies: + tagged-tag: 1.0.0 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unicorn-magic@0.3.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.10)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.10 + + use-sidecar@1.1.3(@types/react@19.2.10)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.10 + + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + web-streams-polyfill@3.3.3: {} + + webpack-sources@3.3.4: {} + + webpack@5.104.1(esbuild@0.27.3): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.4.0(esbuild@0.27.3)(webpack@5.104.1(esbuild@0.27.3)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-pm-runs@1.1.0: {} + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.1 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..297374d --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..1ddedcd Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..71c6955 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/mozhe-api-logo.png b/public/icons/mozhe-api-logo.png new file mode 100644 index 0000000..524ef3a Binary files /dev/null and b/public/icons/mozhe-api-logo.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..9f26d04 Binary files /dev/null and b/public/logo.png differ diff --git a/public/mozhe-api-icon.svg b/public/mozhe-api-icon.svg new file mode 100644 index 0000000..c31e736 --- /dev/null +++ b/public/mozhe-api-icon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repair_recovered_work_owners.js b/repair_recovered_work_owners.js new file mode 100644 index 0000000..92e775a --- /dev/null +++ b/repair_recovered_work_owners.js @@ -0,0 +1,98 @@ +const { Pool } = require('pg'); +require('dotenv').config({ path: '.env.local' }); + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + +(async () => { + const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + const byUniqueCustomModel = await client.query(` + WITH model_owners AS ( + SELECT + params->>'model' AS model, + ARRAY_AGG(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL AND user_id <> $1) AS owner_ids + FROM works + WHERE status = 'completed' + AND params->>'model' LIKE 'custom:%' + AND EXISTS (SELECT 1 FROM profiles p WHERE p.id = works.user_id) + GROUP BY params->>'model' + ), + unique_model_owners AS ( + SELECT model, owner_ids[1] AS owner_id + FROM model_owners + WHERE CARDINALITY(owner_ids) = 1 + ), + updated AS ( + UPDATE works w + SET user_id = umo.owner_id, + updated_at = NOW() + FROM unique_model_owners umo + WHERE w.user_id = $1 + AND w.status = 'completed' + AND w.is_public = true + AND w.params->>'model' = umo.model + RETURNING w.id, w.user_id, w.params->>'model' AS model + ) + SELECT COUNT(*)::int AS fixed_count, json_agg(updated) AS rows + FROM updated + `, [SYSTEM_USER_ID]); + + const byExactPromptTime = await client.query(` + WITH candidates AS ( + SELECT DISTINCT ON (public.id) + public.id AS public_id, + private.user_id AS owner_user_id, + ABS(EXTRACT(EPOCH FROM (private.created_at - public.created_at))) AS time_distance + FROM works public + JOIN works private + ON private.id <> public.id + AND private.user_id IS NOT NULL + AND private.user_id <> $1 + AND COALESCE(private.prompt, '') <> '' + AND private.prompt = public.prompt + AND private.created_at BETWEEN public.created_at - INTERVAL '30 minutes' AND public.created_at + INTERVAL '30 minutes' + JOIN profiles p ON p.id = private.user_id + WHERE public.user_id = $1 + AND public.is_public = true + AND public.status = 'completed' + ORDER BY public.id, time_distance + ), + unambiguous AS ( + SELECT public_id, MIN(owner_user_id::text)::uuid AS owner_user_id + FROM candidates + GROUP BY public_id + HAVING COUNT(DISTINCT owner_user_id) = 1 + ), + updated AS ( + UPDATE works w + SET user_id = unambiguous.owner_user_id, + updated_at = NOW() + FROM unambiguous + WHERE w.id = unambiguous.public_id + AND w.user_id = $1 + RETURNING w.id, w.user_id, w.params->>'model' AS model + ) + SELECT COUNT(*)::int AS fixed_count, json_agg(updated) AS rows + FROM updated + `, [SYSTEM_USER_ID]); + + await client.query('COMMIT'); + + console.log(JSON.stringify({ + byUniqueCustomModel: byUniqueCustomModel.rows[0] || { fixed_count: 0, rows: [] }, + byExactPromptTime: byExactPromptTime.rows[0] || { fixed_count: 0, rows: [] }, + }, null, 2)); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + await pool.end(); + } +})().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/apply-database-patch.sh b/scripts/apply-database-patch.sh new file mode 100644 index 0000000..ad3d105 --- /dev/null +++ b/scripts/apply-database-patch.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" + +if [ -f "${COZE_WORKSPACE_PATH}/.env.local" ]; then + set +u + set -a + # shellcheck disable=SC1091 + source "${COZE_WORKSPACE_PATH}/.env.local" + set +a + set -u +fi + +if [ -z "${LOCAL_DB_URL:-}" ]; then + echo "LOCAL_DB_URL is not set" >&2 + exit 1 +fi + +psql "${LOCAL_DB_URL}" -v ON_ERROR_STOP=1 -f "${COZE_WORKSPACE_PATH}/scripts/database-optimization-patch.sql" diff --git a/scripts/backup-create.sh b/scripts/backup-create.sh new file mode 100644 index 0000000..b19a64f --- /dev/null +++ b/scripts/backup-create.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" +REQUESTED_BACKUP_DIR="${BACKUP_DIR:-}" +REQUESTED_LOCAL_DB_URL="${LOCAL_DB_URL:-}" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +cd "${COZE_WORKSPACE_PATH}" + +if [ -f ".env.local" ]; then + set +u + set -a + # shellcheck disable=SC1091 + source ".env.local" + set +a + set -u +fi + +[ -n "${REQUESTED_LOCAL_DB_URL}" ] && LOCAL_DB_URL="${REQUESTED_LOCAL_DB_URL}" +BACKUP_DIR="${REQUESTED_BACKUP_DIR:-${BACKUP_DIR:-${COZE_WORKSPACE_PATH}/backups}}" +BACKUP_FILE="${BACKUP_DIR}/miaojing-backup-${TIMESTAMP}.tar.gz" +mkdir -p "${BACKUP_DIR}" +chmod 700 "${BACKUP_DIR}" + +if [ -z "${LOCAL_DB_URL:-}" ]; then + echo "LOCAL_DB_URL is required in .env.local or environment." >&2 + exit 1 +fi + +command -v pg_dump >/dev/null 2>&1 || { + echo "pg_dump is required to create backups." >&2 + exit 1 +} + +pg_dump "${LOCAL_DB_URL}" --format=custom --file "${TMP_DIR}/database.dump" + +STORAGE_SOURCE="${LOCAL_STORAGE_DIR:-${COZE_WORKSPACE_PATH}/local-storage}" +if [ -d "${STORAGE_SOURCE}" ]; then + cp -a "${STORAGE_SOURCE}" "${TMP_DIR}/local-storage" +fi + +if [ -f ".env.local" ]; then + cp ".env.local" "${TMP_DIR}/.env.local" +fi + +if [ -f "package.json" ]; then + cp "package.json" "${TMP_DIR}/package.json" +fi + +cat > "${TMP_DIR}/manifest.json" <10 {print $2}' | xargs -r rm -f + +echo "${BACKUP_FILE}" diff --git a/scripts/backup-list.sh b/scripts/backup-list.sh new file mode 100644 index 0000000..7274265 --- /dev/null +++ b/scripts/backup-list.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" +REQUESTED_BACKUP_DIR="${BACKUP_DIR:-}" + +cd "${COZE_WORKSPACE_PATH}" + +if [ -f ".env.local" ]; then + set +u + set -a + # shellcheck disable=SC1091 + source ".env.local" + set +a + set -u +fi + +BACKUP_DIR="${REQUESTED_BACKUP_DIR:-${BACKUP_DIR:-${COZE_WORKSPACE_PATH}/backups}}" + +mkdir -p "${BACKUP_DIR}" +chmod 700 "${BACKUP_DIR}" + +if ! compgen -G "${BACKUP_DIR}/miaojing-backup-*.tar.gz" >/dev/null; then + echo "No backups found in ${BACKUP_DIR}" + exit 0 +fi + +printf '%-40s %-12s %s\n' "FILE" "SIZE" "MODIFIED" +find "${BACKUP_DIR}" -maxdepth 1 -name 'miaojing-backup-*.tar.gz' -type f \ + -printf '%T@ %f %s %TY-%Tm-%Td %TH:%TM\n' \ + | sort -rn \ + | awk '{printf "%-40s %-12s %s %s\n", $2, $3, $4, $5}' diff --git a/scripts/backup-restore.sh b/scripts/backup-restore.sh new file mode 100644 index 0000000..06f63d0 --- /dev/null +++ b/scripts/backup-restore.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" +BACKUP_FILE="${1:-}" +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +if [ -z "${BACKUP_FILE}" ]; then + echo "Usage: pnpm backup:restore " >&2 + exit 2 +fi + +if [ ! -f "${BACKUP_FILE}" ]; then + echo "Backup file not found: ${BACKUP_FILE}" >&2 + exit 2 +fi + +cd "${COZE_WORKSPACE_PATH}" + +if [ -f ".env.local" ]; then + set +u + set -a + # shellcheck disable=SC1091 + source ".env.local" + set +a + set -u +fi + +if [ -z "${LOCAL_DB_URL:-}" ]; then + echo "LOCAL_DB_URL is required in .env.local or environment." >&2 + exit 1 +fi + +command -v pg_restore >/dev/null 2>&1 || { + echo "pg_restore is required to restore backups." >&2 + exit 1 +} + +tar -xzf "${BACKUP_FILE}" -C "${TMP_DIR}" + +if [ ! -f "${TMP_DIR}/database.dump" ]; then + echo "Invalid backup: missing database.dump." >&2 + exit 2 +fi + +pg_restore --clean --if-exists --no-owner --dbname "${LOCAL_DB_URL}" "${TMP_DIR}/database.dump" + +if [ -d "${TMP_DIR}/local-storage" ]; then + STORAGE_TARGET="${LOCAL_STORAGE_DIR:-${COZE_WORKSPACE_PATH}/local-storage}" + rm -rf "${STORAGE_TARGET}" + mkdir -p "$(dirname "${STORAGE_TARGET}")" + cp -a "${TMP_DIR}/local-storage" "${STORAGE_TARGET}" +fi + +if [ -f "${TMP_DIR}/.env.local" ]; then + cp "${TMP_DIR}/.env.local" ".env.local" + chmod 600 ".env.local" +fi + +echo "Restore completed from ${BACKUP_FILE}" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..9685f36 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" + +cd "${COZE_WORKSPACE_PATH}" + +if [ "${INSTALL_DEPS:-0}" = "1" ] || [ ! -d node_modules ]; then + echo "Installing dependencies..." + pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only +else + echo "Skipping dependency install. Set INSTALL_DEPS=1 to force it." +fi + +echo "Building the Next.js project..." +pnpm next build + +echo "Bundling server with tsup..." +pnpm tsup src/server.ts --format cjs --platform node --target node20 --outDir dist --no-splitting --no-minify + +echo "Build completed successfully!" diff --git a/scripts/check-boundaries.sh b/scripts/check-boundaries.sh new file mode 100644 index 0000000..567f1c1 --- /dev/null +++ b/scripts/check-boundaries.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -Eeuo pipefail + +fail=0 + +search_pattern() { + local pattern="$1" + shift + + if command -v rg >/dev/null 2>&1; then + rg -n "$pattern" "$@" || true + else + grep -RInE "$pattern" "$@" || true + fi +} + +check_no_match() { + local label="$1" + local pattern="$2" + shift 2 + local output + + output="$(search_pattern "$pattern" "$@")" + if [ -n "$output" ]; then + echo "Boundary violation: ${label}" >&2 + echo "$output" >&2 + fail=1 + fi +} + +check_no_match \ + "web module must not import server database/storage internals" \ + "@/storage|@/lib/local-storage|@/lib/session-auth|@/lib/admin-auth|@/lib/runtime-env|@/lib/server-crypto" \ + src/modules/web + +check_no_match \ + "console module must not import server database/storage internals directly" \ + "@/storage|@/lib/local-storage|@/lib/runtime-env|@/lib/server-crypto" \ + src/modules/console + +check_no_match \ + "shared module must not depend on app-specific modules" \ + "@/modules/(web|console|api)|@/app/|@/components/admin" \ + src/modules/shared + +if [ "$fail" -ne 0 ]; then + exit 1 +fi + +echo "Module boundaries OK" diff --git a/scripts/database-optimization-patch.sql b/scripts/database-optimization-patch.sql new file mode 100644 index 0000000..fe3075c --- /dev/null +++ b/scripts/database-optimization-patch.sql @@ -0,0 +1,169 @@ +-- Idempotent local PostgreSQL patch for production maintenance. +-- It creates missing application tables and adds indexes used by hot paths. + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +CREATE TABLE IF NOT EXISTS works ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID, + title VARCHAR(255), + type VARCHAR(32) NOT NULL, + prompt TEXT, + negative_prompt TEXT, + params JSONB DEFAULT '{}'::jsonb, + result_url TEXT, + thumbnail_url TEXT, + width INTEGER, + height INTEGER, + duration NUMERIC(6, 2), + is_public BOOLEAN NOT NULL DEFAULT false, + likes_count INTEGER NOT NULL DEFAULT 0, + credits_cost INTEGER NOT NULL DEFAULT 0, + status VARCHAR(32) NOT NULL DEFAULT 'completed', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID, + order_no VARCHAR(64) NOT NULL UNIQUE, + product_type VARCHAR(32) NOT NULL, + product_name VARCHAR(255) NOT NULL, + amount NUMERIC(10, 2) NOT NULL, + credits_amount INTEGER, + status VARCHAR(32) NOT NULL DEFAULT 'pending', + payment_method VARCHAR(32), + paid_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS user_api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID, + provider VARCHAR(64) NOT NULL, + api_url TEXT, + model_name VARCHAR(128), + api_key_encrypted TEXT NOT NULL, + api_key_preview VARCHAR(20), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS work_likes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID, + work_id UUID NOT NULL REFERENCES works(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS generation_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(16) NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'queued', + payload JSONB NOT NULL DEFAULT '{}'::jsonb, + result JSONB, + error TEXT, + user_id UUID, + provider VARCHAR(128), + model_name VARCHAR(255), + api_url TEXT, + progress JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS user_id UUID; +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS provider VARCHAR(128); +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS model_name VARCHAR(255); +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS api_url TEXT; +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS progress JSONB NOT NULL DEFAULT '{}'::jsonb; + +CREATE INDEX IF NOT EXISTS works_user_created_idx ON works (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS works_public_status_created_idx ON works (is_public, status, created_at DESC); +CREATE INDEX IF NOT EXISTS works_public_status_likes_idx ON works (is_public, status, likes_count DESC); +CREATE INDEX IF NOT EXISTS works_type_created_idx ON works (type, created_at DESC); +CREATE INDEX IF NOT EXISTS works_status_created_idx ON works (status, created_at DESC); + +CREATE INDEX IF NOT EXISTS credit_transactions_user_created_idx ON credit_transactions (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS credit_transactions_type_created_idx ON credit_transactions (type, created_at DESC); + +CREATE INDEX IF NOT EXISTS orders_user_created_idx ON orders (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS orders_status_created_idx ON orders (status, created_at DESC); +CREATE INDEX IF NOT EXISTS orders_order_no_idx ON orders (order_no); + +CREATE INDEX IF NOT EXISTS user_api_keys_user_active_idx ON user_api_keys (user_id, is_active); +CREATE INDEX IF NOT EXISTS user_api_keys_provider_idx ON user_api_keys (provider); + +CREATE INDEX IF NOT EXISTS work_likes_user_id_idx ON work_likes (user_id); +CREATE INDEX IF NOT EXISTS work_likes_work_id_idx ON work_likes (work_id); +CREATE UNIQUE INDEX IF NOT EXISTS work_likes_user_work_uniq ON work_likes (user_id, work_id); + +CREATE INDEX IF NOT EXISTS announcements_active_window_idx ON announcements (is_active, starts_at, expires_at); +CREATE INDEX IF NOT EXISTS profiles_email_trgm_idx ON profiles USING GIN (LOWER(email) gin_trgm_ops); +CREATE INDEX IF NOT EXISTS profiles_nickname_trgm_idx ON profiles USING GIN (LOWER(COALESCE(nickname, '')) gin_trgm_ops); +CREATE INDEX IF NOT EXISTS profiles_phone_trgm_idx ON profiles USING GIN (LOWER(COALESCE(phone, '')) gin_trgm_ops); +CREATE INDEX IF NOT EXISTS generation_jobs_status_created_idx ON generation_jobs (status, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_status_updated_idx ON generation_jobs (status, updated_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_running_timeout_idx ON generation_jobs (updated_at) WHERE status = 'running'; +CREATE INDEX IF NOT EXISTS generation_jobs_created_idx ON generation_jobs (created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC); + +CREATE TABLE IF NOT EXISTS platform_log_settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + retention_days INTEGER NOT NULL DEFAULT 30, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +INSERT INTO platform_log_settings (id, retention_days) +VALUES (1, 30) +ON CONFLICT (id) DO NOTHING; + +CREATE TABLE IF NOT EXISTS platform_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(32) NOT NULL, + level VARCHAR(16) NOT NULL DEFAULT 'info', + action VARCHAR(128) NOT NULL, + message TEXT NOT NULL, + user_id UUID, + user_name VARCHAR(255), + user_email VARCHAR(255), + target_type VARCHAR(64), + target_id VARCHAR(255), + ip_address VARCHAR(64), + user_agent TEXT, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS platform_logs_type_created_idx ON platform_logs (type, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_level_created_idx ON platform_logs (level, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_user_created_idx ON platform_logs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_created_idx ON platform_logs (created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_user_name_idx ON platform_logs (LOWER(COALESCE(user_name, ''))); +CREATE INDEX IF NOT EXISTS platform_logs_user_email_idx ON platform_logs (LOWER(COALESCE(user_email, ''))); + +DROP POLICY IF EXISTS "site_config_write_auth" ON site_config; +DROP POLICY IF EXISTS "announcements_write_auth" ON announcements; +DROP POLICY IF EXISTS "site_stats_write_auth" ON site_stats; + +DROP POLICY IF EXISTS "site_config_admin_write" ON site_config; +DROP POLICY IF EXISTS "announcements_admin_write" ON announcements; + +CREATE POLICY "site_config_admin_write" ON site_config FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +CREATE POLICY "announcements_admin_write" ON announcements FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +ANALYZE; diff --git a/scripts/deploy-or-upgrade.sh b/scripts/deploy-or-upgrade.sh new file mode 100644 index 0000000..5661529 --- /dev/null +++ b/scripts/deploy-or-upgrade.sh @@ -0,0 +1,1156 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +APP_NAME="妙境 AI 创作平台" +APP_MARKER=".miaojing-deployment" +DEFAULT_PROJECT_DIR="/opt/miaojingAI" +DEFAULT_DATA_DIR="/var/lib/miaojingAI" +DEFAULT_WEB_PORT="5000" +DEFAULT_API_PORT="5100" +DEFAULT_CONSOLE_PORT="5200" +DEFAULT_ADMIN_ACCOUNT="admin" +DEFAULT_ADMIN_EMAIL="admin@example.com" +DEFAULT_DOMAIN="" +DEFAULT_NODE_MAJOR="24" +MIRRORS=( + "https://registry.npmmirror.com" + "https://registry.npmjs.org" + "https://mirrors.cloud.tencent.com/npm/" + "https://mirrors.huaweicloud.com/repository/npm/" +) +NODE_DIST_MIRRORS=( + "https://npmmirror.com/mirrors/node" + "https://mirrors.tuna.tsinghua.edu.cn/nodejs-release" + "https://mirrors.cloud.tencent.com/nodejs-release" + "https://mirrors.huaweicloud.com/nodejs" + "https://nodejs.org/dist" +) + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LOG_FILE="" +PROJECT_DIR="" +DATA_DIR="" +WEB_PORT="" +API_PORT="" +CONSOLE_PORT="" +ADMIN_ACCOUNT="" +ADMIN_EMAIL="" +ADMIN_PASSWORD="" +LOCAL_DB_URL_INPUT="" +MODE="" +BACKUP_FILE="" +SERVER_HOST_IP="" +EXISTING_LOCAL_STORAGE_DIR="" +APP_PUBLIC_URL="" +NODE_MAJOR="${DEPLOY_NODE_MAJOR:-${DEFAULT_NODE_MAJOR}}" +NODE_INSTALL_ROOT="${DEPLOY_NODE_INSTALL_DIR:-}" +NODE_BIN_DIR="" +NODE_VERSION="" +NPM_BIN="npm" + +log() { + local message="$*" + if [ -n "${LOG_FILE:-}" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${message}" | tee -a "${LOG_FILE}" + else + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${message}" + fi +} + +log_pipe() { + if [ -n "${LOG_FILE:-}" ]; then + tee -a "${LOG_FILE}" + else + cat + fi +} + +fail() { + local message="$*" + echo + echo "❌ 部署失败:${message}" | tee -a "${LOG_FILE:-/dev/null}" >&2 + if [ -n "${LOG_FILE:-}" ]; then + echo "详细日志:${LOG_FILE}" >&2 + fi + if [ -n "${BACKUP_FILE:-}" ]; then + echo "已生成升级前备份:${BACKUP_FILE}" >&2 + echo "如需回滚,可在部署目录执行:pnpm backup:restore \"${BACKUP_FILE}\"" >&2 + fi + exit 1 +} + +trap 'fail "脚本执行中断,请查看上方错误日志。"' ERR + +require_command() { + local command_name="$1" + local install_hint="$2" + if ! command -v "${command_name}" >/dev/null 2>&1; then + fail "缺少命令 ${command_name}。${install_hint}" + fi +} + +prompt_value() { + local var_name="$1" + local label="$2" + local default_value="$3" + local value="" + read -r -p "${label} [${default_value}]: " value + printf -v "${var_name}" '%s' "${value:-$default_value}" +} + +prompt_secret() { + local var_name="$1" + local label="$2" + local value="" + while [ -z "${value}" ]; do + read -r -s -p "${label}: " value + echo + if [ -z "${value}" ]; then + echo "该项不能为空,请重新输入。" + fi + done + printf -v "${var_name}" '%s' "${value}" +} + +random_hex() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 32 + else + head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' + fi +} + +env_quote() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//\$/\\\$}" + value="${value//\`/\\\`}" + value="${value//$'\n'/\\n}" + printf '"%s"' "${value}" +} + +env_get_value() { + local key="$1" + local file="$2" + local line value + if [ ! -f "${file}" ]; then + return 1 + fi + + while IFS= read -r line || [ -n "${line}" ]; do + case "${line}" in + "${key}="*) + value="${line#*=}" + value="${value%$'\r'}" + if [[ "${value}" == \"*\" ]] && [[ "${value}" == *\" ]]; then + value="${value:1:${#value}-2}" + value="${value//\\\"/\"}" + value="${value//\\\\/\\}" + fi + printf '%s\n' "${value}" + return 0 + ;; + esac + done < "${file}" + + return 1 +} + +env_set_value() { + local key="$1" + local value="$2" + local file="$3" + local quoted tmp_file + quoted="$(env_quote "${value}")" + tmp_file="$(mktemp)" + + if [ -f "${file}" ]; then + awk -v key="${key}" -v replacement="${key}=${quoted}" ' + BEGIN { found = 0 } + $0 ~ "^" key "=" { + if (found == 0) print replacement + found = 1 + next + } + { print } + END { + if (found == 0) print replacement + } + ' "${file}" > "${tmp_file}" + else + printf '%s=%s\n' "${key}" "${quoted}" > "${tmp_file}" + fi + + mv "${tmp_file}" "${file}" +} + +js_quote() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\'/\\\'}" + value="${value//$'\n'/\\n}" + printf "'%s'" "${value}" +} + +detect_host_ip() { + SERVER_HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" + SERVER_HOST_IP="${SERVER_HOST_IP:-127.0.0.1}" +} + +prepend_node_path() { + if [ -n "${NODE_BIN_DIR:-}" ] && [ -d "${NODE_BIN_DIR}" ]; then + case ":${PATH}:" in + *":${NODE_BIN_DIR}:"*) ;; + *) export PATH="${NODE_BIN_DIR}:${PATH}" ;; + esac + NPM_BIN="${NODE_BIN_DIR}/npm" + else + NPM_BIN="npm" + fi +} + +node_major_version() { + node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || printf '0' +} + +node_version_matches_target() { + command -v node >/dev/null 2>&1 && [ "$(node_major_version)" = "${NODE_MAJOR}" ] +} + +node_platform_arch() { + local machine + machine="$(uname -m)" + case "${machine}" in + x86_64|amd64) + printf 'linux-x64' + ;; + aarch64|arm64) + printf 'linux-arm64' + ;; + *) + fail "暂不支持当前 CPU 架构:${machine}。部署脚本支持 x86_64/amd64 和 arm64/aarch64。" + ;; + esac +} + +detect_latest_node_version() { + local mirror="$1" + curl -fsSL "${mirror}/index.json" \ + | sed -n "s/.*\"version\"[[:space:]]*:[[:space:]]*\"\\(v${NODE_MAJOR}\\.[0-9][^\"]*\\)\".*/\\1/p" \ + | head -n 1 +} + +install_node_from_mirrors() { + local platform_arch version mirror archive_url tmp_dir archive install_dir node_bin + platform_arch="$(node_platform_arch)" + NODE_INSTALL_ROOT="${NODE_INSTALL_ROOT:-${DATA_DIR}/node}" + + mkdir -p "${NODE_INSTALL_ROOT}" + tmp_dir="$(mktemp -d)" + + for mirror in "${NODE_DIST_MIRRORS[@]}"; do + log "尝试从 Node.js 镜像源获取 ${NODE_MAJOR}.x LTS:${mirror}" + version="$(NODE_MAJOR="${NODE_MAJOR}" detect_latest_node_version "${mirror}" || true)" + if [ -z "${version}" ]; then + log "当前镜像源未获取到 Node.js ${NODE_MAJOR}.x 版本索引,切换下一个源。" + continue + fi + + archive="node-${version}-${platform_arch}.tar.xz" + archive_url="${mirror}/${version}/${archive}" + log "准备下载 Node.js ${version}:${archive_url}" + if ! curl -fL --retry 2 --connect-timeout 15 -o "${tmp_dir}/${archive}" "${archive_url}" 2>&1 | log_pipe; then + log "Node.js 下载失败,切换下一个镜像源。" + continue + fi + + install_dir="${NODE_INSTALL_ROOT}/node-${version}-${platform_arch}" + rm -rf "${install_dir}" + mkdir -p "${install_dir}" + tar -xJf "${tmp_dir}/${archive}" -C "${NODE_INSTALL_ROOT}" + NODE_BIN_DIR="${install_dir}/bin" + node_bin="${NODE_BIN_DIR}/node" + if [ -x "${node_bin}" ]; then + prepend_node_path + NODE_VERSION="$("${node_bin}" -v)" + log "Node.js ${NODE_VERSION} 安装完成,路径:${NODE_BIN_DIR}" + rm -rf "${tmp_dir}" + return 0 + fi + done + + rm -rf "${tmp_dir}" + return 1 +} + +ensure_node_runtime() { + if ! [[ "${NODE_MAJOR}" =~ ^(22|24)$ ]]; then + fail "DEPLOY_NODE_MAJOR 只允许设置为 22 或 24,当前值:${NODE_MAJOR}" + fi + + if node_version_matches_target; then + NODE_VERSION="$(node -v)" + log "Node.js 版本符合生产要求:${NODE_VERSION}" + return 0 + fi + + if command -v node >/dev/null 2>&1; then + log "当前 Node.js 版本为 $(node -v),将自动安装并切换到 Node.js ${NODE_MAJOR}.x LTS。" + else + log "未检测到 Node.js,将自动安装 Node.js ${NODE_MAJOR}.x LTS。" + fi + + install_node_from_mirrors || fail "Node.js ${NODE_MAJOR}.x LTS 自动安装失败,请检查网络或手动安装后重试。" + + if ! node_version_matches_target; then + fail "Node.js 已安装但版本校验失败,当前版本:$(node -v 2>/dev/null || printf '未检测到')" + fi +} + +normalize_data_dir_from_storage() { + local storage_dir="$1" + if [ -z "${storage_dir}" ]; then + return 1 + fi + + storage_dir="$(realpath -m "${storage_dir}")" + if [ "$(basename "${storage_dir}")" = "storage" ]; then + dirname "${storage_dir}" + else + printf '%s\n' "${storage_dir}" + fi +} + +read_marker_value() { + local key="$1" + local file="$2" + local marker_key marker_value + if [ ! -f "${file}" ]; then + return 1 + fi + + while IFS='=' read -r marker_key marker_value; do + if [ "${marker_key}" = "${key}" ]; then + printf '%s\n' "${marker_value}" + return 0 + fi + done < "${file}" + + return 1 +} + +detect_existing_deployment() { + if [ -f "${PROJECT_DIR}/package.json" ] && { [ -f "${PROJECT_DIR}/${APP_MARKER}" ] || [ -f "${PROJECT_DIR}/.env.local" ]; }; then + MODE="upgrade" + else + MODE="install" + fi +} + +load_existing_defaults() { + local marker_data_dir="" + if [ -f "${PROJECT_DIR}/${APP_MARKER}" ]; then + marker_data_dir="$(read_marker_value "data_dir" "${PROJECT_DIR}/${APP_MARKER}" || true)" + fi + + if [ -f "${PROJECT_DIR}/.env.local" ]; then + # shellcheck disable=SC1090 + set +u; set -a; source "${PROJECT_DIR}/.env.local"; set +a; set -u + if [ -n "${LOCAL_STORAGE_DIR:-}" ]; then + EXISTING_LOCAL_STORAGE_DIR="$(realpath -m "${LOCAL_STORAGE_DIR}")" + DATA_DIR="$(normalize_data_dir_from_storage "${LOCAL_STORAGE_DIR}")" + elif [ -n "${BACKUP_DIR:-}" ]; then + DATA_DIR="$(dirname "$(realpath -m "${BACKUP_DIR}")")" + elif [ -n "${marker_data_dir}" ]; then + DATA_DIR="${marker_data_dir}" + fi + LOCAL_DB_URL_INPUT="${LOCAL_DB_URL:-${LOCAL_DB_URL_INPUT:-postgresql://postgres:postgres@localhost:5432/miaojing}}" + WEB_PORT="${DEPLOY_RUN_PORT:-${WEB_PORT:-$DEFAULT_WEB_PORT}}" + API_PORT="${MIAOJING_API_PORT:-${API_PORT:-$DEFAULT_API_PORT}}" + CONSOLE_PORT="${MIAOJING_CONSOLE_PORT:-${CONSOLE_PORT:-$DEFAULT_CONSOLE_PORT}}" + ADMIN_EMAIL="${ADMIN_EMAIL:-${DEFAULT_ADMIN_EMAIL}}" + elif [ -n "${marker_data_dir}" ]; then + DATA_DIR="${marker_data_dir}" + fi + + if [ -z "${APP_PUBLIC_URL}" ] && [ -f "${PROJECT_DIR}/.env.local" ]; then + APP_PUBLIC_URL="$(env_get_value "NEXT_PUBLIC_APP_URL" "${PROJECT_DIR}/.env.local" || true)" + fi + if [ -z "${APP_PUBLIC_URL}" ] && [ -f "${PROJECT_DIR}/.env.local" ]; then + APP_PUBLIC_URL="$(env_get_value "APP_BASE_URL" "${PROJECT_DIR}/.env.local" || true)" + fi + + if [ -z "${EXISTING_LOCAL_STORAGE_DIR}" ] && [ -d "${PROJECT_DIR}/local-storage" ]; then + EXISTING_LOCAL_STORAGE_DIR="$(realpath -m "${PROJECT_DIR}/local-storage")" + elif [ -z "${EXISTING_LOCAL_STORAGE_DIR}" ] && [ -n "${marker_data_dir}" ] && [ -d "${marker_data_dir}/storage" ]; then + EXISTING_LOCAL_STORAGE_DIR="$(realpath -m "${marker_data_dir}/storage")" + fi +} + +validate_port() { + local label="$1" + local value="$2" + if ! [[ "${value}" =~ ^[0-9]+$ ]] || [ "${value}" -lt 1 ] || [ "${value}" -gt 65535 ]; then + fail "${label}必须是 1-65535 之间的数字。" + fi +} + +validate_inputs() { + validate_port "前端访问端口" "${WEB_PORT}" + validate_port "后端 API 内部端口" "${API_PORT}" + validate_port "管理后台内部端口" "${CONSOLE_PORT}" + + if [ "${WEB_PORT}" = "${API_PORT}" ] || [ "${WEB_PORT}" = "${CONSOLE_PORT}" ] || [ "${API_PORT}" = "${CONSOLE_PORT}" ]; then + fail "前端、后端 API、管理后台端口不能重复。" + fi + + if [ -z "${ADMIN_ACCOUNT}" ] || [ -z "${ADMIN_EMAIL}" ]; then + fail "管理员账号和管理员邮箱不能为空。" + fi + + if ! [[ "${ADMIN_EMAIL}" =~ ^[^[:space:]@]+@[^[:space:]@]+[.][^[:space:]@]+$ ]]; then + fail "管理员邮箱格式不正确。" + fi + + if [ -z "${LOCAL_DB_URL_INPUT}" ]; then + fail "PostgreSQL 连接地址不能为空。" + fi + + if [ -n "${APP_PUBLIC_URL}" ] && ! [[ "${APP_PUBLIC_URL}" =~ ^https?://[^[:space:]]+$ ]]; then + fail "正式访问地址必须是 http:// 或 https:// 开头的完整地址。" + fi + + if [ "${MODE}" = "install" ] && [ "${ADMIN_PASSWORD}" = "admin123" ]; then + fail "生产环境不允许使用默认管理员密码 admin123,请设置高强度密码。" + fi +} + +collect_inputs() { + echo "==============================================" + echo "${APP_NAME} 一键部署/升级脚本" + echo "==============================================" + echo "请按提示填写部署参数。直接回车将使用默认值。" + echo + + prompt_value PROJECT_DIR "项目部署目录" "${DEPLOY_PROJECT_DIR:-$DEFAULT_PROJECT_DIR}" + PROJECT_DIR="$(realpath -m "${PROJECT_DIR}")" + DATA_DIR="${DEPLOY_DATA_DIR:-$DEFAULT_DATA_DIR}" + WEB_PORT="${DEPLOY_WEB_PORT:-$DEFAULT_WEB_PORT}" + API_PORT="${DEPLOY_API_PORT:-$DEFAULT_API_PORT}" + CONSOLE_PORT="${DEPLOY_CONSOLE_PORT:-$DEFAULT_CONSOLE_PORT}" + ADMIN_ACCOUNT="${DEPLOY_ADMIN_ACCOUNT:-$DEFAULT_ADMIN_ACCOUNT}" + ADMIN_EMAIL="${DEPLOY_ADMIN_EMAIL:-$DEFAULT_ADMIN_EMAIL}" + LOCAL_DB_URL_INPUT="${DEPLOY_LOCAL_DB_URL:-postgresql://postgres:postgres@localhost:5432/miaojing}" + + detect_existing_deployment + load_existing_defaults + + if [ "${MODE}" = "install" ]; then + echo + echo "检测结果:目标目录未部署项目,将执行首次部署流程。" + else + echo + echo "检测结果:目标目录已存在部署,将执行安全升级流程。" + fi + + prompt_value DATA_DIR "数据存储目录" "${DATA_DIR}" + DATA_DIR="$(realpath -m "${DATA_DIR}")" + prompt_value WEB_PORT "前端访问端口" "${WEB_PORT}" + prompt_value API_PORT "后端 API 内部端口" "${API_PORT}" + prompt_value CONSOLE_PORT "管理后台内部端口" "${CONSOLE_PORT}" + prompt_value ADMIN_ACCOUNT "管理员账号/昵称" "${ADMIN_ACCOUNT}" + prompt_value ADMIN_EMAIL "管理员邮箱" "${ADMIN_EMAIL}" + prompt_value APP_PUBLIC_URL "正式访问地址(有域名请填 https://域名,留空则使用服务器IP和端口)" "${APP_PUBLIC_URL:-$DEFAULT_DOMAIN}" + + if [ "${MODE}" = "install" ]; then + prompt_secret ADMIN_PASSWORD "管理员密码" + prompt_value LOCAL_DB_URL_INPUT "PostgreSQL 连接地址" "${LOCAL_DB_URL_INPUT}" + else + read -r -s -p "管理员密码(升级时可留空表示不修改): " ADMIN_PASSWORD + echo + prompt_value LOCAL_DB_URL_INPUT "PostgreSQL 连接地址" "${LOCAL_DB_URL_INPUT}" + fi + + validate_inputs +} + +prepare_log() { + mkdir -p "${DATA_DIR}/logs" + LOG_FILE="${DATA_DIR}/logs/deploy-$(date +%Y%m%d-%H%M%S).log" + touch "${LOG_FILE}" + chmod 600 "${LOG_FILE}" + log "日志文件:${LOG_FILE}" +} + +check_prerequisites() { + log "检查运行依赖..." + require_command tar "请安装 tar。" + require_command rsync "请安装 rsync。" + require_command curl "请安装 curl。" + ensure_node_runtime + prepend_node_path + require_command node "Node.js 自动安装后仍不可用,请检查 PATH。" + require_command npm "Node.js 自动安装后 npm 仍不可用,请检查 Node.js 安装包。" + require_command psql "请安装 PostgreSQL 客户端,例如 postgresql-client。" + require_command pg_dump "请安装 PostgreSQL 客户端,例如 postgresql-client。" + + log "当前使用 Node.js:$(node -v),npm:$(npm -v)" + + if ! command -v pnpm >/dev/null 2>&1; then + log "未检测到 pnpm,准备通过 npm 安装 pnpm@9..." + install_pnpm + fi + + if ! command -v pm2 >/dev/null 2>&1; then + log "未检测到 pm2,准备通过 npm 安装 pm2..." + install_pm2 + fi +} + +npm_install_global_with_mirrors() { + local package_name="$1" + local mirror + for mirror in "${MIRRORS[@]}"; do + log "尝试使用镜像源安装 ${package_name}:${mirror}" + if "${NPM_BIN}" --registry="${mirror}" install -g "${package_name}" 2>&1 | log_pipe; then + log "${package_name} 安装成功。" + return 0 + fi + log "镜像源不可用或安装失败,切换下一个源。" + done + return 1 +} + +install_pnpm() { + npm_install_global_with_mirrors "pnpm@9" || fail "pnpm 安装失败,请检查网络或手动安装。" +} + +install_pm2() { + npm_install_global_with_mirrors "pm2" || fail "pm2 安装失败,请检查网络或手动安装。" +} + +install_dependencies_with_mirrors() { + local mirror + for mirror in "${MIRRORS[@]}"; do + log "尝试使用依赖镜像源:${mirror}" + pnpm config set registry "${mirror}" >/dev/null 2>&1 || true + if pnpm install --frozen-lockfile --reporter=append-only 2>&1 | log_pipe; then + log "依赖安装成功,使用源:${mirror}" + return 0 + fi + log "依赖安装失败,切换下一个镜像源。" + done + fail "所有依赖镜像源均安装失败,请检查网络。" +} + +sync_project_files() { + if [ "${SOURCE_DIR}" = "${PROJECT_DIR}" ]; then + log "源码目录与部署目录一致,跳过代码同步。" + return 0 + fi + + log "同步项目代码到部署目录:${PROJECT_DIR}" + mkdir -p "${PROJECT_DIR}" + rsync -a --delete \ + --exclude ".git" \ + --exclude "node_modules" \ + --exclude ".next" \ + --exclude "dist" \ + --exclude "backups" \ + --exclude "local-storage" \ + --exclude ".env.local" \ + --exclude ".codex_tmp" \ + "${SOURCE_DIR}/" "${PROJECT_DIR}/" 2>&1 | log_pipe +} + +migrate_local_storage() { + if [ "${MODE}" != "upgrade" ]; then + return 0 + fi + + local target_storage="${DATA_DIR}/storage" + if [ -z "${EXISTING_LOCAL_STORAGE_DIR}" ] || [ ! -d "${EXISTING_LOCAL_STORAGE_DIR}" ]; then + log "未检测到旧版本地存储目录,跳过本地存储迁移。" + return 0 + fi + + if [ "$(realpath -m "${EXISTING_LOCAL_STORAGE_DIR}")" = "$(realpath -m "${target_storage}")" ]; then + log "本地存储目录未变化,跳过迁移:${target_storage}" + return 0 + fi + + log "同步旧本地存储到新的持久化目录:${EXISTING_LOCAL_STORAGE_DIR} -> ${target_storage}" + mkdir -p "${target_storage}" + rsync -a "${EXISTING_LOCAL_STORAGE_DIR}/" "${target_storage}/" 2>&1 | log_pipe +} + +write_env_file() { + local env_file encryption_key jwt_secret generation_secret invite_code admin_default_password app_base_url existing_admin_password + env_file="${PROJECT_DIR}/.env.local" + encryption_key="$(env_get_value "DATA_ENCRYPTION_KEY" "${env_file}" || random_hex)" + jwt_secret="$(env_get_value "JWT_SECRET" "${env_file}" || random_hex)" + generation_secret="$(env_get_value "GENERATION_INTERNAL_SECRET" "${env_file}" || random_hex)" + invite_code="$(env_get_value "ADMIN_INVITE_CODE" "${env_file}" || true)" + invite_code="${invite_code:-miaojing-admin-$(random_hex | cut -c1-8)}" + existing_admin_password="$(env_get_value "ADMIN_DEFAULT_PASSWORD" "${env_file}" || true)" + admin_default_password="${ADMIN_PASSWORD:-${existing_admin_password}}" + app_base_url="${APP_PUBLIC_URL:-http://${SERVER_HOST_IP}:${WEB_PORT}}" + + mkdir -p "${DATA_DIR}/storage" "${DATA_DIR}/backups" + + if [ ! -f "${env_file}" ]; then + cat > "${env_file}" < "${PROJECT_DIR}/ecosystem.config.cjs" <&1 | log_pipe > "${DATA_DIR}/logs/.last-backup-path" + BACKUP_FILE="$(tail -n 1 "${DATA_DIR}/logs/.last-backup-path" || true)" + log "升级前备份完成:${BACKUP_FILE}" + else + log "未找到旧版备份脚本,执行基础文件备份。" + BACKUP_FILE="${DATA_DIR}/backups/miaojing-files-$(date +%Y%m%d-%H%M%S).tar.gz" + if [ -d "${DATA_DIR}/storage" ]; then + tar -czf "${BACKUP_FILE}" -C "${PROJECT_DIR}" .env.local -C "${DATA_DIR}" storage + else + tar -czf "${BACKUP_FILE}" -C "${PROJECT_DIR}" .env.local + fi + log "基础备份完成:${BACKUP_FILE}" + fi +} + +initialize_database() { + log "检查数据库连接..." + psql "${LOCAL_DB_URL_INPUT}" -v ON_ERROR_STOP=1 -c "SELECT 1;" >/dev/null + + log "执行数据库结构初始化/升级 SQL(幂等,不会删除用户数据)..." + psql "${LOCAL_DB_URL_INPUT}" -v ON_ERROR_STOP=1 -f "${PROJECT_DIR}/scripts/init-database.sql" 2>&1 | log_pipe + + if [ -f "${PROJECT_DIR}/scripts/database-optimization-patch.sql" ]; then + psql "${LOCAL_DB_URL_INPUT}" -v ON_ERROR_STOP=1 -f "${PROJECT_DIR}/scripts/database-optimization-patch.sql" 2>&1 | log_pipe + fi + + apply_runtime_schema_patch +} + +apply_runtime_schema_patch() { + log "补齐生产运行所需的动态配置表..." + psql "${LOCAL_DB_URL_INPUT}" -v ON_ERROR_STOP=1 <<'SQL' 2>&1 | log_pipe +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE SCHEMA IF NOT EXISTS auth; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'auth' AND p.proname = 'uid' + ) THEN + EXECUTE 'CREATE FUNCTION auth.uid() RETURNS UUID AS $fn$ SELECT NULLIF(current_setting(''request.jwt.claim.sub'', true), '''')::UUID; $fn$ LANGUAGE SQL STABLE'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'auth' AND p.proname = 'role' + ) THEN + EXECUTE 'CREATE FUNCTION auth.role() RETURNS TEXT AS $fn$ SELECT COALESCE(NULLIF(current_setting(''request.jwt.claim.role'', true), ''''), ''anon''); $fn$ LANGUAGE SQL STABLE'; + END IF; +END $$; + +ALTER TABLE profiles + ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS email_bound_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS email_sender_domain VARCHAR(255), + ADD COLUMN IF NOT EXISTS preferred_theme VARCHAR(16) NOT NULL DEFAULT 'dark'; + +UPDATE profiles + SET preferred_theme = 'dark' + WHERE preferred_theme IS NULL + OR preferred_theme NOT IN ('dark', 'light'); + +ALTER TABLE site_config + ADD COLUMN IF NOT EXISTS site_description TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS site_keywords TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS announcement TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS membership_enabled BOOLEAN NOT NULL DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS terms_of_service TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS privacy_policy TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS about_us TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS help_center TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS filing_info TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS filing_url TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS public_security_filing_info TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS public_security_filing_url TEXT NOT NULL DEFAULT ''; + +ALTER TABLE works + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS views_count INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE announcements + ADD COLUMN IF NOT EXISTS type VARCHAR(32) NOT NULL DEFAULT 'site'; + +ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128); +ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT ''; +ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image'; + +CREATE TABLE IF NOT EXISTS system_api_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider VARCHAR(128), + name VARCHAR(255) NOT NULL, + api_url TEXT NOT NULL DEFAULT '', + model_name VARCHAR(255) NOT NULL, + note TEXT NOT NULL DEFAULT '', + api_key_encrypted TEXT NOT NULL DEFAULT '', + api_key_preview VARCHAR(64) NOT NULL DEFAULT '', + type VARCHAR(16) NOT NULL DEFAULT 'image', + credits_per_use INTEGER NOT NULL DEFAULT 10, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS system_api_configs_active_type_sort_idx ON system_api_configs (is_active, type, sort_order); + +CREATE TABLE IF NOT EXISTS payment_methods ( + id VARCHAR(64) PRIMARY KEY, + type VARCHAR(32) NOT NULL, + name VARCHAR(128) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + public_config JSONB NOT NULL DEFAULT '{}'::jsonb, + secret_config_encrypted JSONB NOT NULL DEFAULT '{}'::jsonb, + secret_config_preview JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); +INSERT INTO payment_methods (id, type, name, is_active) VALUES + ('pm-alipay', 'alipay', '支付宝', true), + ('pm-wechat', 'wechat', '微信支付', false), + ('pm-manual', 'manual', '手动转账', false), + ('pm-stripe', 'stripe', 'Stripe', false) +ON CONFLICT (id) DO NOTHING; + +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS user_id UUID; +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS provider VARCHAR(128); +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS model_name VARCHAR(255); +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS api_url TEXT; +ALTER TABLE generation_jobs ADD COLUMN IF NOT EXISTS progress JSONB NOT NULL DEFAULT '{}'::jsonb; +CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC); + +ALTER TABLE site_config ADD COLUMN IF NOT EXISTS log_retention_days INTEGER NOT NULL DEFAULT 30; +UPDATE site_config SET log_retention_days = LEAST(90, GREATEST(1, log_retention_days)); + +CREATE TABLE IF NOT EXISTS platform_log_settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + retention_days INTEGER NOT NULL DEFAULT 30, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +INSERT INTO platform_log_settings (id, retention_days) +VALUES (1, 30) +ON CONFLICT (id) DO NOTHING; + +CREATE TABLE IF NOT EXISTS platform_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(32) NOT NULL, + level VARCHAR(16) NOT NULL DEFAULT 'info', + action VARCHAR(128) NOT NULL, + message TEXT NOT NULL, + user_id UUID, + user_name VARCHAR(255), + user_email VARCHAR(255), + target_type VARCHAR(64), + target_id VARCHAR(255), + ip_address VARCHAR(64), + user_agent TEXT, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS platform_logs_type_created_idx ON platform_logs (type, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_level_created_idx ON platform_logs (level, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_user_created_idx ON platform_logs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_created_idx ON platform_logs (created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_user_name_idx ON platform_logs (LOWER(COALESCE(user_name, ''))); +CREATE INDEX IF NOT EXISTS platform_logs_user_email_idx ON platform_logs (LOWER(COALESCE(user_email, ''))); + +CREATE TABLE IF NOT EXISTS email_settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + enabled BOOLEAN NOT NULL DEFAULT FALSE, + smtp_host VARCHAR(255), + smtp_port INTEGER NOT NULL DEFAULT 465, + smtp_secure BOOLEAN NOT NULL DEFAULT TRUE, + smtp_user VARCHAR(255), + smtp_password_encrypted TEXT, + smtp_password_preview VARCHAR(64), + from_email VARCHAR(255), + from_name VARCHAR(255), + reply_to VARCHAR(255), + app_name VARCHAR(120), + app_base_url TEXT, + logo_url TEXT, + contact_email VARCHAR(255), + copyright TEXT, + code_length INTEGER NOT NULL DEFAULT 6, + code_charset VARCHAR(32) NOT NULL DEFAULT 'alphanumeric', + code_ttl_minutes INTEGER NOT NULL DEFAULT 5, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS email_verification_codes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL, + code_hash TEXT NOT NULL, + type VARCHAR(32) NOT NULL, + user_id UUID, + ip_address VARCHAR(64), + attempts INTEGER NOT NULL DEFAULT 0, + max_attempts INTEGER NOT NULL DEFAULT 5, + is_used BOOLEAN NOT NULL DEFAULT FALSE, + locked_until TIMESTAMPTZ, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS email_send_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL, + type VARCHAR(64) NOT NULL, + ip_address VARCHAR(64), + status VARCHAR(32) NOT NULL, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS email_codes_email_type_idx ON email_verification_codes (LOWER(email), type, created_at DESC); +CREATE INDEX IF NOT EXISTS email_codes_ip_created_idx ON email_verification_codes (ip_address, created_at DESC); +CREATE INDEX IF NOT EXISTS email_send_logs_email_created_idx ON email_send_logs (LOWER(email), created_at DESC); +CREATE INDEX IF NOT EXISTS email_send_logs_ip_created_idx ON email_send_logs (ip_address, created_at DESC); +SQL +} + +ensure_admin_user() { + if [ -z "${ADMIN_PASSWORD:-}" ] && [ "${MODE}" = "upgrade" ]; then + log "升级模式未输入管理员密码,跳过管理员密码更新。" + return 0 + fi + + log "创建/更新管理员账号..." + psql "${LOCAL_DB_URL_INPUT}" \ + -v ON_ERROR_STOP=1 \ + -v admin_email="${ADMIN_EMAIL}" \ + -v admin_account="${ADMIN_ACCOUNT}" \ + -v admin_password="${ADMIN_PASSWORD}" <<'SQL' 2>&1 | log_pipe +CREATE TEMP TABLE _deploy_admin_input ( + email TEXT NOT NULL, + account TEXT NOT NULL, + password TEXT NOT NULL +); + +INSERT INTO _deploy_admin_input (email, account, password) +VALUES (:'admin_email', :'admin_account', :'admin_password'); + +DO $$ +DECLARE + r RECORD; + v_admin_id UUID; +BEGIN + SELECT * INTO r FROM _deploy_admin_input LIMIT 1; + + SELECT id INTO v_admin_id FROM profiles WHERE lower(email) = lower(r.email) LIMIT 1; + IF v_admin_id IS NULL THEN + SELECT id INTO v_admin_id FROM auth.users WHERE lower(email) = lower(r.email) LIMIT 1; + END IF; + IF v_admin_id IS NULL THEN + v_admin_id := gen_random_uuid(); + END IF; + + INSERT INTO auth.users (id, email, password_hash, raw_user_meta_data, created_at) + VALUES ( + v_admin_id, + r.email, + crypt(r.password, gen_salt('bf')), + jsonb_build_object('nickname', r.account), + NOW() + ) + ON CONFLICT (email) DO UPDATE SET + password_hash = EXCLUDED.password_hash, + raw_user_meta_data = EXCLUDED.raw_user_meta_data; + + SELECT id INTO v_admin_id FROM auth.users WHERE lower(email) = lower(r.email) LIMIT 1; + + INSERT INTO profiles ( + id, email, nickname, role, membership_tier, credits_balance, + daily_quota_limit, daily_quota_used, is_active, + email_verified, email_verified_at, email_bound_at, email_sender_domain + ) + VALUES ( + v_admin_id, r.email, r.account, 'admin', 'enterprise', + 9999, 999, 0, true, true, NOW(), NOW(), split_part(r.email, '@', 2) + ) + ON CONFLICT (id) DO UPDATE SET + email = EXCLUDED.email, + nickname = EXCLUDED.nickname, + role = 'admin', + membership_tier = 'enterprise', + credits_balance = GREATEST(profiles.credits_balance, 9999), + daily_quota_limit = GREATEST(profiles.daily_quota_limit, 999), + is_active = true, + email_verified = true, + email_verified_at = COALESCE(profiles.email_verified_at, NOW()), + email_bound_at = COALESCE(profiles.email_bound_at, NOW()), + email_sender_domain = COALESCE(NULLIF(profiles.email_sender_domain, ''), EXCLUDED.email_sender_domain), + updated_at = NOW(); +END $$; +SQL +} + +build_project() { + log "开始安装依赖..." + cd "${PROJECT_DIR}" + install_dependencies_with_mirrors + + log "开始生产构建..." + pnpm run check:boundaries 2>&1 | log_pipe + pnpm run build 2>&1 | log_pipe +} + +run_security_audit() { + log "执行生产依赖漏洞扫描..." + cd "${PROJECT_DIR}" + local mirror audit_status + audit_status=1 + + for mirror in "${MIRRORS[@]}"; do + log "尝试使用漏洞库源执行 pnpm audit:${mirror}" + if pnpm audit --prod --audit-level=high --registry="${mirror}" 2>&1 | log_pipe; then + audit_status=0 + break + fi + log "当前源审计失败或发现高危漏洞,继续尝试下一个源。" + done + + if [ "${audit_status}" -ne 0 ]; then + fail "生产依赖漏洞扫描未通过。请先处理 high/critical 级别漏洞后再上线。" + fi + + if ! pnpm audit --prod --audit-level=moderate --registry="https://registry.npmjs.org" 2>&1 | log_pipe; then + log "提醒:仍存在 moderate 级别漏洞。脚本不会阻断升级,但正式上线前建议升级相关依赖链并重新构建验证。" + fi +} + +start_services() { + log "启动/重载 PM2 服务..." + cd "${PROJECT_DIR}" + pm2 startOrReload ecosystem.config.cjs --update-env 2>&1 | log_pipe + pm2 save 2>&1 | log_pipe || true +} + +wait_for_health() { + log "等待服务启动并执行健康检查..." + local api_url="http://127.0.0.1:${WEB_PORT}/api/health" + local console_url="http://127.0.0.1:${WEB_PORT}/console" + local attempt + for attempt in $(seq 1 30); do + if curl -fsS "${api_url}" >/dev/null 2>&1 && curl -fsS "${console_url}" >/dev/null 2>&1; then + log "健康检查通过:前端、后端 API、管理后台均可访问。" + return 0 + fi + sleep 2 + done + fail "健康检查失败,请检查 PM2 日志:pm2 logs miaojing-web" +} + +mark_deployment() { + cat > "${PROJECT_DIR}/${APP_MARKER}" </dev/null | awk -v port="${DEPLOY_RUN_PORT}" '$4 ~ ":"port"$"' | grep -o 'pid=[0-9]*' | cut -d= -f2 | paste -sd' ' - || true) + if [[ -z "${pids}" ]]; then + echo "Port ${DEPLOY_RUN_PORT} is free." + return + fi + echo "Port ${DEPLOY_RUN_PORT} in use by PIDs: ${pids} (SIGKILL)" + echo "${pids}" | xargs -I {} kill -9 {} + sleep 1 + pids=$(ss -H -lntp 2>/dev/null | awk -v port="${DEPLOY_RUN_PORT}" '$4 ~ ":"port"$"' | grep -o 'pid=[0-9]*' | cut -d= -f2 | paste -sd' ' - || true) + if [[ -n "${pids}" ]]; then + echo "Warning: port ${DEPLOY_RUN_PORT} still busy after SIGKILL, PIDs: ${pids}" + else + echo "Port ${DEPLOY_RUN_PORT} cleared." + fi +} + +echo "Clearing port ${PORT} before start." +kill_port_if_listening +echo "Starting HTTP service on port ${PORT} for dev..." + +PORT=$PORT pnpm tsx watch src/server.ts diff --git a/scripts/init-database.sql b/scripts/init-database.sql new file mode 100644 index 0000000..1beb926 --- /dev/null +++ b/scripts/init-database.sql @@ -0,0 +1,663 @@ +-- ============================================================ +-- 妙境 AI 创作平台 — 数据库初始化脚本 +-- 适用于: PostgreSQL 14+ (Supabase / 自托管) +-- 执行方式: 在 Supabase SQL Editor 或 psql 中运行 +-- ============================================================ + +-- 0. 启用必要扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- 1. 创建 auth 模式和 users 表 +CREATE SCHEMA IF NOT EXISTS auth; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'auth' AND p.proname = 'uid' + ) THEN + EXECUTE 'CREATE FUNCTION auth.uid() RETURNS UUID AS $fn$ SELECT NULLIF(current_setting(''request.jwt.claim.sub'', true), '''')::UUID; $fn$ LANGUAGE SQL STABLE'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'auth' AND p.proname = 'role' + ) THEN + EXECUTE 'CREATE FUNCTION auth.role() RETURNS TEXT AS $fn$ SELECT COALESCE(NULLIF(current_setting(''request.jwt.claim.role'', true), ''''), ''anon''); $fn$ LANGUAGE SQL STABLE'; + END IF; +END $$; + +CREATE TABLE IF NOT EXISTS auth.users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE, + password_hash TEXT, + raw_user_meta_data JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS auth_users_email_idx ON auth.users (email); + +-- ============================================================ +-- 1. 用户资料表 (profiles) +-- 与 Supabase Auth 的 auth.users 表关联 +-- ============================================================ +CREATE TABLE IF NOT EXISTS profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL UNIQUE, + nickname VARCHAR(128), + avatar_url TEXT, + phone VARCHAR(20), + role VARCHAR(32) NOT NULL DEFAULT 'user', -- guest, user, vip, enterprise_admin, enterprise_member, admin + membership_tier VARCHAR(32) NOT NULL DEFAULT 'free', -- free, basic, pro, enterprise + membership_expires_at TIMESTAMPTZ, + credits_balance INTEGER NOT NULL DEFAULT 0, + daily_quota_used INTEGER NOT NULL DEFAULT 0, + daily_quota_limit INTEGER NOT NULL DEFAULT 5, + is_active BOOLEAN NOT NULL DEFAULT true, + email_verified BOOLEAN NOT NULL DEFAULT false, + email_verified_at TIMESTAMPTZ, + email_bound_at TIMESTAMPTZ, + email_sender_domain VARCHAR(255), + preferred_theme VARCHAR(16) NOT NULL DEFAULT 'dark', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS profiles_email_idx ON profiles (email); +CREATE INDEX IF NOT EXISTS profiles_role_idx ON profiles (role); + +-- ============================================================ +-- 2. 创作作品表 (works) +-- ============================================================ +CREATE TABLE IF NOT EXISTS works ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + title VARCHAR(255), + type VARCHAR(32) NOT NULL, -- text2img, img2img, text2video, img2video + prompt TEXT, + negative_prompt TEXT, + params JSONB, -- 生成参数 (画面比例、分辨率、模型等) + result_url TEXT, -- 生成文件的 URL + thumbnail_url TEXT, + width INTEGER, + height INTEGER, + duration NUMERIC(6, 2), -- 视频时长 (秒) + is_public BOOLEAN NOT NULL DEFAULT false, + likes_count INTEGER NOT NULL DEFAULT 0, + views_count INTEGER NOT NULL DEFAULT 0, + credits_cost INTEGER NOT NULL DEFAULT 0, + status VARCHAR(32) NOT NULL DEFAULT 'completed', -- pending, processing, completed, failed + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS works_user_id_idx ON works (user_id); +CREATE INDEX IF NOT EXISTS works_type_idx ON works (type); +CREATE INDEX IF NOT EXISTS works_is_public_idx ON works (is_public); +CREATE INDEX IF NOT EXISTS works_created_at_idx ON works (created_at); +CREATE INDEX IF NOT EXISTS works_status_idx ON works (status); + +-- ============================================================ +-- 3. 积分记录表 (credit_transactions) +-- ============================================================ +CREATE TABLE IF NOT EXISTS credit_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + amount INTEGER NOT NULL, -- 正数=入账, 负数=消费 + balance_after INTEGER NOT NULL, + type VARCHAR(32) NOT NULL, -- purchase, consume, gift, reward, refund + description VARCHAR(500), + related_work_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS credit_transactions_user_id_idx ON credit_transactions (user_id); +CREATE INDEX IF NOT EXISTS credit_transactions_type_idx ON credit_transactions (type); +CREATE INDEX IF NOT EXISTS credit_transactions_created_at_idx ON credit_transactions (created_at); + +-- ============================================================ +-- 4. 订单表 (orders) +-- ============================================================ +CREATE TABLE IF NOT EXISTS orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + order_no VARCHAR(64) NOT NULL UNIQUE, + product_type VARCHAR(32) NOT NULL, -- membership, credits, api + product_name VARCHAR(255) NOT NULL, + amount NUMERIC(10, 2) NOT NULL, + credits_amount INTEGER, -- 购买的积分数 + status VARCHAR(32) NOT NULL DEFAULT 'pending', -- pending, paid, cancelled, refunded + payment_method VARCHAR(32), -- wechat, alipay, stripe + paid_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS orders_user_id_idx ON orders (user_id); +CREATE INDEX IF NOT EXISTS orders_order_no_idx ON orders (order_no); +CREATE INDEX IF NOT EXISTS orders_status_idx ON orders (status); +CREATE INDEX IF NOT EXISTS orders_created_at_idx ON orders (created_at); + +-- ============================================================ +-- 5. 生成任务队列表 (generation_jobs) +-- ============================================================ +CREATE TABLE IF NOT EXISTS generation_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(16) NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'queued', + payload JSONB NOT NULL DEFAULT '{}'::jsonb, + result JSONB, + error TEXT, + user_id UUID, + provider VARCHAR(128), + model_name VARCHAR(255), + api_url TEXT, + progress JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS generation_jobs_status_created_idx ON generation_jobs (status, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_status_updated_idx ON generation_jobs (status, updated_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_running_timeout_idx ON generation_jobs (updated_at) WHERE status = 'running'; +CREATE INDEX IF NOT EXISTS generation_jobs_created_idx ON generation_jobs (created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC); + +-- ============================================================ +-- 6. 用户自定义 API 密钥表 (user_api_keys) +-- ============================================================ +CREATE TABLE IF NOT EXISTS user_api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + provider VARCHAR(64) NOT NULL, -- openai, stabilityai, runway, etc. + api_url TEXT, -- 完整 API 端点 URL + model_name VARCHAR(128), -- 具体模型名称 + api_key_encrypted TEXT NOT NULL, -- 加密存储的 API Key + api_key_preview VARCHAR(20), -- Key 尾号 (如 sk-...4f3e) + supplier_name VARCHAR(128), + note TEXT NOT NULL DEFAULT '', + type VARCHAR(16) NOT NULL DEFAULT 'image', + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS user_api_keys_user_id_idx ON user_api_keys (user_id); +CREATE INDEX IF NOT EXISTS user_api_keys_provider_idx ON user_api_keys (provider); + +-- ============================================================ +-- 7. 作品点赞表 (work_likes) +-- ============================================================ +CREATE TABLE IF NOT EXISTS work_likes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + work_id UUID NOT NULL REFERENCES works(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS work_likes_user_id_idx ON work_likes (user_id); +CREATE INDEX IF NOT EXISTS work_likes_work_id_idx ON work_likes (work_id); + +-- 唯一约束:每个用户对每个作品只能点赞一次 +CREATE UNIQUE INDEX IF NOT EXISTS work_likes_user_work_uniq ON work_likes (user_id, work_id); + +-- ============================================================ +-- 8. 网站配置表 (site_config) +-- ============================================================ +CREATE TABLE IF NOT EXISTS site_config ( + id INTEGER PRIMARY KEY DEFAULT 1, + site_name VARCHAR(128) NOT NULL DEFAULT '妙境', + site_tab_title VARCHAR(255) NOT NULL DEFAULT '妙境 - AI创作平台', + site_description TEXT NOT NULL DEFAULT '', + site_keywords TEXT NOT NULL DEFAULT '', + logo_url TEXT, + favicon_url TEXT, + announcement TEXT NOT NULL DEFAULT '', + membership_enabled BOOLEAN NOT NULL DEFAULT TRUE, + terms_of_service TEXT NOT NULL DEFAULT '', + privacy_policy TEXT NOT NULL DEFAULT '', + about_us TEXT NOT NULL DEFAULT '', + help_center TEXT NOT NULL DEFAULT '', + filing_info TEXT NOT NULL DEFAULT '', + filing_url TEXT NOT NULL DEFAULT '', + public_security_filing_info TEXT NOT NULL DEFAULT '', + public_security_filing_url TEXT NOT NULL DEFAULT '', + log_retention_days INTEGER NOT NULL DEFAULT 30, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +-- 插入默认配置 +INSERT INTO site_config (id, site_name, site_tab_title) +VALUES (1, '妙境', '妙境 - AI创作平台') +ON CONFLICT (id) DO NOTHING; + +-- ============================================================ +-- 9. 公告表 (announcements) +-- ============================================================ +CREATE TABLE IF NOT EXISTS announcements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, -- 支持 Markdown + type VARCHAR(32) NOT NULL DEFAULT 'site', + is_active BOOLEAN NOT NULL DEFAULT true, + starts_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_by UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS announcements_is_active_idx ON announcements (is_active); +CREATE INDEX IF NOT EXISTS announcements_expires_at_idx ON announcements (expires_at); + +-- ============================================================ +-- 10. 网站统计表 (site_stats) +-- ============================================================ +CREATE TABLE IF NOT EXISTS site_stats ( + id INTEGER PRIMARY KEY DEFAULT 1, + total_visits BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +INSERT INTO site_stats (id, total_visits) VALUES (1, 0) ON CONFLICT (id) DO NOTHING; + +-- ============================================================ +-- 11. 平台日志 +-- ============================================================ +CREATE TABLE IF NOT EXISTS platform_log_settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + retention_days INTEGER NOT NULL DEFAULT 30, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +INSERT INTO platform_log_settings (id, retention_days) +VALUES (1, 30) +ON CONFLICT (id) DO NOTHING; + +CREATE TABLE IF NOT EXISTS platform_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(32) NOT NULL, + level VARCHAR(16) NOT NULL DEFAULT 'info', + action VARCHAR(128) NOT NULL, + message TEXT NOT NULL, + user_id UUID, + user_name VARCHAR(255), + user_email VARCHAR(255), + target_type VARCHAR(64), + target_id VARCHAR(255), + ip_address VARCHAR(64), + user_agent TEXT, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS platform_logs_type_created_idx ON platform_logs (type, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_level_created_idx ON platform_logs (level, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_user_created_idx ON platform_logs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS platform_logs_created_idx ON platform_logs (created_at DESC); + +-- ============================================================ +-- 12. API 供应商与推荐模型配置 +-- ============================================================ +CREATE TABLE IF NOT EXISTS api_providers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(128) NOT NULL UNIQUE, + default_api_url TEXT, + default_model VARCHAR(255), + type VARCHAR(16) NOT NULL DEFAULT 'image', + website TEXT, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS model_recommendations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + model_name VARCHAR(255) NOT NULL, + display_name VARCHAR(255), + type VARCHAR(16) NOT NULL DEFAULT 'image', + provider_id UUID REFERENCES api_providers(id) ON DELETE SET NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS api_providers_active_sort_idx ON api_providers (is_active, sort_order); +CREATE INDEX IF NOT EXISTS model_recommendations_active_type_sort_idx ON model_recommendations (is_active, type, sort_order); +CREATE INDEX IF NOT EXISTS model_recommendations_provider_idx ON model_recommendations (provider_id); + +INSERT INTO api_providers (name, default_api_url, default_model, type, website, is_active, sort_order) +VALUES + ('硅基流动', 'https://api.siliconflow.cn/v1/images/generations', 'black-forest-labs/FLUX.1-schnell', 'image', 'https://cloud.siliconflow.cn', true, 10), + ('mozheAPI', 'https://openai.mozhevip.top', '', 'image', 'https://openai.mozhevip.top', true, 20), + ('OpenAI', 'https://api.openai.com/v1/images/generations', 'dall-e-3', 'image', NULL, true, 30), + ('Stability AI', 'https://api.stability.ai/v1/generation/stable-diffusion-xl/text-to-image', 'stable-diffusion-xl', 'image', NULL, true, 40), + ('Midjourney', '', 'midjourney-v6', 'image', NULL, true, 50), + ('Runway', 'https://api.runwayml.com/v1/image_to_video', 'gen-3-alpha', 'video', NULL, true, 60), + ('Pika', '', 'pika-1.0', 'video', NULL, true, 70), + ('Kling', '', 'kling-v1', 'video', NULL, true, 80), + ('DeepSeek', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 'text', NULL, true, 90), + ('OpenAI GPT', 'https://api.openai.com/v1/chat/completions', 'gpt-4o', 'text', NULL, true, 100), + ('自定义', '', '', 'image', NULL, true, 999) +ON CONFLICT (name) DO NOTHING; + +INSERT INTO model_recommendations (model_name, display_name, type, provider_id, is_active, sort_order) +SELECT 'gpt-image-2', 'gpt-image-2', 'image', NULL, true, 10 +WHERE NOT EXISTS ( + SELECT 1 FROM model_recommendations + WHERE model_name = 'gpt-image-2' AND type = 'image' AND provider_id IS NULL +); + +-- ============================================================ +-- 兼容旧版本库结构的幂等补丁 +-- ============================================================ +ALTER TABLE profiles + ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS email_bound_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS email_sender_domain VARCHAR(255), + ADD COLUMN IF NOT EXISTS preferred_theme VARCHAR(16) NOT NULL DEFAULT 'dark'; + +UPDATE profiles + SET preferred_theme = 'dark' + WHERE preferred_theme IS NULL + OR preferred_theme NOT IN ('dark', 'light'); + +ALTER TABLE works + ADD COLUMN IF NOT EXISTS views_count INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ; + +ALTER TABLE user_api_keys + ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128), + ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image'; + +ALTER TABLE site_config + ADD COLUMN IF NOT EXISTS site_description TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS site_keywords TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS announcement TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS membership_enabled BOOLEAN NOT NULL DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS terms_of_service TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS privacy_policy TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS about_us TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS help_center TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS filing_info TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS filing_url TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS public_security_filing_info TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS public_security_filing_url TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS log_retention_days INTEGER NOT NULL DEFAULT 30; + +ALTER TABLE generation_jobs + ADD COLUMN IF NOT EXISTS user_id UUID, + ADD COLUMN IF NOT EXISTS provider VARCHAR(128), + ADD COLUMN IF NOT EXISTS model_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS api_url TEXT, + ADD COLUMN IF NOT EXISTS progress JSONB NOT NULL DEFAULT '{}'::jsonb; + +CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC); + +CREATE TABLE IF NOT EXISTS system_api_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider VARCHAR(128), + name VARCHAR(255) NOT NULL, + api_url TEXT NOT NULL DEFAULT '', + model_name VARCHAR(255) NOT NULL, + note TEXT NOT NULL DEFAULT '', + api_key_encrypted TEXT NOT NULL DEFAULT '', + api_key_preview VARCHAR(64) NOT NULL DEFAULT '', + type VARCHAR(16) NOT NULL DEFAULT 'image', + credits_per_use INTEGER NOT NULL DEFAULT 10, + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS system_api_configs_active_type_sort_idx ON system_api_configs (is_active, type, sort_order); + +CREATE TABLE IF NOT EXISTS payment_methods ( + id VARCHAR(64) PRIMARY KEY, + type VARCHAR(32) NOT NULL, + name VARCHAR(128) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + public_config JSONB NOT NULL DEFAULT '{}'::jsonb, + secret_config_encrypted JSONB NOT NULL DEFAULT '{}'::jsonb, + secret_config_preview JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +INSERT INTO payment_methods (id, type, name, is_active) VALUES + ('pm-alipay', 'alipay', '支付宝', true), + ('pm-wechat', 'wechat', '微信支付', false), + ('pm-manual', 'manual', '手动转账', false), + ('pm-stripe', 'stripe', 'Stripe', false) +ON CONFLICT (id) DO NOTHING; + +CREATE INDEX IF NOT EXISTS platform_logs_user_name_idx ON platform_logs (LOWER(COALESCE(user_name, ''))); +CREATE INDEX IF NOT EXISTS platform_logs_user_email_idx ON platform_logs (LOWER(COALESCE(user_email, ''))); + +ALTER TABLE announcements + ADD COLUMN IF NOT EXISTS type VARCHAR(32) NOT NULL DEFAULT 'site'; + +ALTER TABLE platform_log_settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE platform_logs ENABLE ROW LEVEL SECURITY; +ALTER TABLE api_providers ENABLE ROW LEVEL SECURITY; +ALTER TABLE model_recommendations ENABLE ROW LEVEL SECURITY; +ALTER TABLE system_api_configs ENABLE ROW LEVEL SECURITY; +ALTER TABLE payment_methods ENABLE ROW LEVEL SECURITY; + +-- ============================================================ +-- Row Level Security (RLS) 策略 +-- ============================================================ + +-- 启用所有表的 RLS +ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE works ENABLE ROW LEVEL SECURITY; +ALTER TABLE credit_transactions ENABLE ROW LEVEL SECURITY; +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_api_keys ENABLE ROW LEVEL SECURITY; +ALTER TABLE work_likes ENABLE ROW LEVEL SECURITY; +ALTER TABLE site_config ENABLE ROW LEVEL SECURITY; +ALTER TABLE announcements ENABLE ROW LEVEL SECURITY; +ALTER TABLE site_stats ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "profiles_read_own" ON profiles; +DROP POLICY IF EXISTS "profiles_update_own" ON profiles; +DROP POLICY IF EXISTS "profiles_admin_all" ON profiles; +DROP POLICY IF EXISTS "works_read_public" ON works; +DROP POLICY IF EXISTS "works_insert_own" ON works; +DROP POLICY IF EXISTS "works_update_own" ON works; +DROP POLICY IF EXISTS "works_delete_own" ON works; +DROP POLICY IF EXISTS "works_admin_all" ON works; +DROP POLICY IF EXISTS "credit_transactions_read_own" ON credit_transactions; +DROP POLICY IF EXISTS "credit_transactions_admin_all" ON credit_transactions; +DROP POLICY IF EXISTS "orders_read_own" ON orders; +DROP POLICY IF EXISTS "orders_insert_own" ON orders; +DROP POLICY IF EXISTS "orders_admin_all" ON orders; +DROP POLICY IF EXISTS "user_api_keys_read_own" ON user_api_keys; +DROP POLICY IF EXISTS "user_api_keys_insert_own" ON user_api_keys; +DROP POLICY IF EXISTS "user_api_keys_update_own" ON user_api_keys; +DROP POLICY IF EXISTS "user_api_keys_delete_own" ON user_api_keys; +DROP POLICY IF EXISTS "work_likes_read_all" ON work_likes; +DROP POLICY IF EXISTS "work_likes_insert_own" ON work_likes; +DROP POLICY IF EXISTS "work_likes_delete_own" ON work_likes; +DROP POLICY IF EXISTS "site_config_read_all" ON site_config; +DROP POLICY IF EXISTS "site_config_write_auth" ON site_config; +DROP POLICY IF EXISTS "site_config_admin_write" ON site_config; +DROP POLICY IF EXISTS "announcements_read_all" ON announcements; +DROP POLICY IF EXISTS "announcements_write_auth" ON announcements; +DROP POLICY IF EXISTS "announcements_admin_write" ON announcements; +DROP POLICY IF EXISTS "site_stats_read_all" ON site_stats; +DROP POLICY IF EXISTS "site_stats_write_auth" ON site_stats; + +-- profiles: 用户可读自己的资料,管理员可读写所有 +CREATE POLICY "profiles_read_own" ON profiles FOR SELECT USING (auth.uid() = id); +CREATE POLICY "profiles_update_own" ON profiles FOR UPDATE USING (auth.uid() = id); +CREATE POLICY "profiles_admin_all" ON profiles FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- works: 用户可管理自己的作品,公开作品所有人可读 +CREATE POLICY "works_read_public" ON works FOR SELECT USING (is_public = true OR auth.uid() = user_id); +CREATE POLICY "works_insert_own" ON works FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "works_update_own" ON works FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY "works_delete_own" ON works FOR DELETE USING (auth.uid() = user_id); +CREATE POLICY "works_admin_all" ON works FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- credit_transactions: 用户可读自己的记录 +CREATE POLICY "credit_transactions_read_own" ON credit_transactions FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "credit_transactions_admin_all" ON credit_transactions FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- orders: 用户可读自己的订单 +CREATE POLICY "orders_read_own" ON orders FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "orders_insert_own" ON orders FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "orders_admin_all" ON orders FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- user_api_keys: 用户可管理自己的密钥 +CREATE POLICY "user_api_keys_read_own" ON user_api_keys FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "user_api_keys_insert_own" ON user_api_keys FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "user_api_keys_update_own" ON user_api_keys FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY "user_api_keys_delete_own" ON user_api_keys FOR DELETE USING (auth.uid() = user_id); + +-- work_likes: 认证用户可点赞,所有人可读 +CREATE POLICY "work_likes_read_all" ON work_likes FOR SELECT USING (true); +CREATE POLICY "work_likes_insert_own" ON work_likes FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "work_likes_delete_own" ON work_likes FOR DELETE USING (auth.uid() = user_id); + +-- site_config: 所有人可读,认证用户可写 (管理员操作通过 service role key) +CREATE POLICY "site_config_read_all" ON site_config FOR SELECT USING (true); +CREATE POLICY "site_config_admin_write" ON site_config FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- announcements: 所有人可读,认证用户可写 (管理员操作) +CREATE POLICY "announcements_read_all" ON announcements FOR SELECT USING (true); +CREATE POLICY "announcements_admin_write" ON announcements FOR ALL USING ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +) WITH CHECK ( + EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin') +); + +-- site_stats: 公开读,访问量递增走 SECURITY DEFINER 函数 +CREATE POLICY "site_stats_read_all" ON site_stats FOR SELECT USING (true); + +-- ============================================================ +-- Supabase Storage 桶 (通过 Supabase Dashboard 或 API 创建) +-- ============================================================ +-- 需要在 Supabase Dashboard 中手动创建以下 Storage 桶: +-- 1. site-assets (公开读) — 存放网站 Logo、Favicon +-- 2. works (私有) — 存放用户生成的图片/视频文件 +-- +-- 或者通过 SQL (需要 service_role 权限): +-- INSERT INTO storage.buckets (id, name, public) VALUES ('site-assets', 'site-assets', true) ON CONFLICT DO NOTHING; +-- INSERT INTO storage.buckets (id, name, public) VALUES ('works', 'works', false) ON CONFLICT DO NOTHING; + +-- ============================================================ +-- 触发器: 自动更新 updated_at 字段 +-- ============================================================ +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS profiles_updated_at ON profiles; +DROP TRIGGER IF EXISTS works_updated_at ON works; +DROP TRIGGER IF EXISTS orders_updated_at ON orders; +DROP TRIGGER IF EXISTS user_api_keys_updated_at ON user_api_keys; +DROP TRIGGER IF EXISTS site_config_updated_at ON site_config; +DROP TRIGGER IF EXISTS announcements_updated_at ON announcements; + +CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER works_updated_at BEFORE UPDATE ON works FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER orders_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER user_api_keys_updated_at BEFORE UPDATE ON user_api_keys FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER site_config_updated_at BEFORE UPDATE ON site_config FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER announcements_updated_at BEFORE UPDATE ON announcements FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- ============================================================ +-- 触发器: 新用户注册时自动创建 profile +-- (仅在使用 Supabase Auth 时生效) +-- ============================================================ +CREATE OR REPLACE FUNCTION handle_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO profiles (id, email, nickname, role, membership_tier, credits_balance, daily_quota_limit) + VALUES ( + NEW.id, + NEW.email, + COALESCE(NEW.raw_user_meta_data->>'nickname', split_part(NEW.email, '@', 1)), + 'user', + 'free', + 10, -- 新用户赠送 10 积分 + 5 -- 每日配额 5 次 + ) + ON CONFLICT (id) DO NOTHING; + -- 记录注册赠送积分 + INSERT INTO credit_transactions (user_id, amount, balance_after, type, description) + SELECT NEW.id, 10, 10, 'gift', '新用户注册奖励' + WHERE NOT EXISTS ( + SELECT 1 FROM credit_transactions + WHERE user_id = NEW.id AND type = 'gift' AND description = '新用户注册奖励' + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; + +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + +-- ============================================================ +-- 初始化管理员账户 (可选) +-- 请在注册管理员后,手动执行以下 SQL 将角色设为 admin: +-- UPDATE profiles SET role = 'admin' WHERE email = 'your-admin@example.com'; +-- ============================================================ + +-- ============================================================ +-- 原子递增访问量的 SQL 函数 +-- ============================================================ +CREATE OR REPLACE FUNCTION increment_visits() +RETURNS BIGINT AS $$ +DECLARE + new_count BIGINT; +BEGIN + UPDATE site_stats SET total_visits = total_visits + 1, updated_at = now() WHERE id = 1 + RETURNING total_visits INTO new_count; + RETURN new_count; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 完成 +SELECT 'Database initialization completed successfully!' AS status; diff --git a/scripts/prepare.sh b/scripts/prepare.sh new file mode 100644 index 0000000..0e1abc9 --- /dev/null +++ b/scripts/prepare.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" + +cd "${COZE_WORKSPACE_PATH}" + +echo "Installing dependencies..." +pnpm install --prefer-frozen-lockfile --prefer-offline --loglevel debug --reporter=append-only +if command -v coze > /dev/null 2>&1 && coze check-bins --help > /dev/null 2>&1; then + coze check-bins --fix +fi diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..612793b --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -Eeuo pipefail + +COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}" + +# Load environment variables from .env.local if it exists. PM2 role-specific +# values are restored afterwards so backend/console services keep their ports. +PM2_DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-}" +PM2_APP_RUNTIME_ROLE="${APP_RUNTIME_ROLE:-}" +PM2_BACKEND_INTERNAL_URL="${BACKEND_INTERNAL_URL:-}" +PM2_CONSOLE_INTERNAL_URL="${CONSOLE_INTERNAL_URL:-}" + +if [ -f "${COZE_WORKSPACE_PATH}/.env.local" ]; then + set +u + set -a + # shellcheck disable=SC1091 + source "${COZE_WORKSPACE_PATH}/.env.local" + set +a + set -u +fi + +[ -n "${PM2_DEPLOY_RUN_PORT}" ] && DEPLOY_RUN_PORT="${PM2_DEPLOY_RUN_PORT}" +[ -n "${PM2_APP_RUNTIME_ROLE}" ] && APP_RUNTIME_ROLE="${PM2_APP_RUNTIME_ROLE}" +[ -n "${PM2_BACKEND_INTERNAL_URL}" ] && BACKEND_INTERNAL_URL="${PM2_BACKEND_INTERNAL_URL}" +[ -n "${PM2_CONSOLE_INTERNAL_URL}" ] && CONSOLE_INTERNAL_URL="${PM2_CONSOLE_INTERNAL_URL}" + +if [ -n "${DEPLOY_NODE_BIN_DIR:-}" ] && [ -d "${DEPLOY_NODE_BIN_DIR}" ]; then + export PATH="${DEPLOY_NODE_BIN_DIR}:${PATH}" +fi + +PORT=${1:-5000} +DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-$PORT}" +APP_RUNTIME_ROLE="${APP_RUNTIME_ROLE:-full}" + + +start_service() { + cd "${COZE_WORKSPACE_PATH}" + echo "Starting ${APP_RUNTIME_ROLE} HTTP service on port ${DEPLOY_RUN_PORT} for deploy..." + echo "COZE_PROJECT_ENV: ${COZE_PROJECT_ENV}" + export NODE_ENV="${NODE_ENV:-production}" + export APP_RUNTIME_ROLE + PORT=${DEPLOY_RUN_PORT} node dist/server.js +} + +echo "Starting ${APP_RUNTIME_ROLE} HTTP service on port ${DEPLOY_RUN_PORT} for deploy..." +start_service diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..b3fe560 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,5 @@ +import { SitePolicyPage } from '@/components/site-policy-page'; + +export default function AboutPage() { + return ; +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..ece68f8 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function AdminRedirectPage() { + redirect('/console'); +} diff --git a/src/app/api/admin/clear-users/route.ts b/src/app/api/admin/clear-users/route.ts new file mode 100644 index 0000000..6241601 --- /dev/null +++ b/src/app/api/admin/clear-users/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { requireAdmin } from '@/lib/admin-auth'; + +const DEFAULT_ADMIN_EMAIL = 'admin@example.com'; + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + if (process.env.ENABLE_DANGER_ADMIN_CLEAR_USERS !== 'true') { + return NextResponse.json( + { error: '生产环境已默认禁用清空用户数据功能。如确需执行,请临时设置 ENABLE_DANGER_ADMIN_CLEAR_USERS=true 并完成备份后再操作。' }, + { status: 403 }, + ); + } + + const body = await request.json(); + const { password } = body; + + const adminPassword = process.env.ADMIN_DEFAULT_PASSWORD || 'admin123'; + + if (password !== adminPassword) { + return NextResponse.json({ error: '管理员密码错误' }, { status: 401 }); + } + + const client = await getDbClient(); + + try { + await client.query('BEGIN'); + + const adminResult = await client.query( + `SELECT id, email, nickname FROM profiles + WHERE role = 'admin' AND is_active = true + ORDER BY CASE WHEN email = $1 THEN 0 ELSE 1 END, created_at ASC + LIMIT 1`, + [DEFAULT_ADMIN_EMAIL], + ); + + if (adminResult.rows.length === 0) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: '未找到可保留的系统管理员账号,已拒绝清理' }, { status: 409 }); + } + + const admin = adminResult.rows[0]; + + await client.query('DELETE FROM credit_transactions WHERE user_id <> $1', [admin.id]); + await client.query('DELETE FROM work_likes WHERE user_id <> $1', [admin.id]); + await client.query('DELETE FROM works WHERE user_id <> $1', [admin.id]); + await client.query('DELETE FROM user_api_keys WHERE user_id <> $1', [admin.id]); + await client.query('DELETE FROM orders WHERE user_id IS NOT NULL AND user_id <> $1', [admin.id]); + await client.query('DELETE FROM profiles WHERE id <> $1', [admin.id]); + await client.query('DELETE FROM auth.users WHERE id <> $1', [admin.id]); + + await client.query( + `UPDATE profiles + SET email = $2, + nickname = COALESCE(NULLIF(nickname, ''), $3), + role = 'admin', + membership_tier = 'enterprise', + credits_balance = GREATEST(COALESCE(credits_balance, 0), 9999), + daily_quota_limit = GREATEST(COALESCE(daily_quota_limit, 0), 999), + daily_quota_used = 0, + is_active = true, + updated_at = NOW() + WHERE id = $1`, + [admin.id, admin.email || DEFAULT_ADMIN_EMAIL, admin.nickname || '管理员'], + ); + + await client.query('COMMIT'); + return NextResponse.json({ success: true, message: '所有非系统管理员用户数据已清除,系统管理员已保留' }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '清除用户数据失败'; + console.error('[Clear Users Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/admin/dashboard/route.ts b/src/app/api/admin/dashboard/route.ts new file mode 100644 index 0000000..480a7bd --- /dev/null +++ b/src/app/api/admin/dashboard/route.ts @@ -0,0 +1,285 @@ +import { NextRequest, NextResponse } from 'next/server'; +import type { PoolClient, QueryResult } from 'pg'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +type DbRow = Record; + +async function safeQuery(client: PoolClient, label: string, sql: string, params: unknown[] = []): Promise> { + try { + return await client.query(sql, params); + } catch (error) { + console.error(`[admin/dashboard] ${label} failed:`, error); + return { rows: [], rowCount: 0, command: 'SELECT', oid: 0, fields: [] }; + } +} + +function numberValue(value: unknown): number { + const parsed = Number(value ?? 0); + return Number.isFinite(parsed) ? parsed : 0; +} + +function firstRow(result: QueryResult): DbRow { + return result.rows[0] || {}; +} + +function statusCount(rows: DbRow[], status: string): number { + const row = rows.find(item => item.status === status); + return numberValue(row?.count); +} + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const client = await getDbClient(); + try { + const [ + platformResult, + userResult, + workResult, + taskStatusResult, + latestTaskResult, + orderStatusResult, + orderRevenueResult, + latestOrderResult, + storageResult, + logResult, + providerResult, + recommendationResult, + userApiKeyResult, + announcementResult, + ] = await Promise.all([ + safeQuery(client, 'platform summary', ` + SELECT + COALESCE((SELECT total_visits FROM site_stats WHERE id = 1 LIMIT 1), 0)::bigint AS total_visits, + NOW() AS database_time + `), + safeQuery(client, 'user summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE COALESCE(is_active, true) = true)::int AS active, + COUNT(*) FILTER (WHERE COALESCE(is_active, true) = false)::int AS disabled, + COUNT(*) FILTER (WHERE COALESCE(role, 'user') IN ('admin', 'enterprise_admin'))::int AS admins, + COUNT(*) FILTER ( + WHERE COALESCE(role, 'user') = 'vip' + OR COALESCE(membership_tier, 'free') NOT IN ('free', '') + )::int AS members, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '7 days')::int AS created_7d + FROM profiles + `), + safeQuery(client, 'work summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE is_public = true)::int AS public, + COUNT(*) FILTER (WHERE is_public = false)::int AS private, + COUNT(*) FILTER (WHERE status = 'completed')::int AS completed, + COUNT(*) FILTER (WHERE status = 'failed')::int AS failed, + COUNT(*) FILTER (WHERE result_url IS NOT NULL AND result_url <> '')::int AS with_result_url, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '7 days')::int AS created_7d, + COUNT(*) FILTER (WHERE type = 'text2img')::int AS text2img, + COUNT(*) FILTER (WHERE type = 'img2img')::int AS img2img, + COUNT(*) FILTER (WHERE type = 'text2video')::int AS text2video, + COUNT(*) FILTER (WHERE type = 'img2video')::int AS img2video + FROM works + `), + safeQuery(client, 'task status summary', ` + SELECT status, COUNT(*)::int AS count + FROM generation_jobs + GROUP BY status + `), + safeQuery(client, 'latest tasks', ` + SELECT id, type, status, error, created_at, updated_at + FROM generation_jobs + ORDER BY created_at DESC + LIMIT 6 + `), + safeQuery(client, 'order status summary', ` + SELECT status, COUNT(*)::int AS count + FROM orders + GROUP BY status + `), + safeQuery(client, 'order revenue summary', ` + SELECT + COALESCE(SUM(amount) FILTER (WHERE status = 'paid'), 0)::numeric AS paid_revenue, + COALESCE(SUM(amount) FILTER ( + WHERE status = 'paid' AND COALESCE(paid_at, created_at) >= NOW() - INTERVAL '7 days' + ), 0)::numeric AS paid_revenue_7d + FROM orders + `), + safeQuery(client, 'latest orders', ` + SELECT id, order_no, product_name, amount, status, created_at + FROM orders + ORDER BY created_at DESC + LIMIT 6 + `), + safeQuery(client, 'storage health', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE result_url IS NOT NULL AND result_url <> '')::int AS persisted + FROM works + `), + safeQuery(client, 'log health', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE level = 'error')::int AS errors, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '24 hours')::int AS created_24h + FROM platform_logs + `), + safeQuery(client, 'provider summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE is_active = true)::int AS active, + COUNT(*) FILTER (WHERE is_active = false)::int AS inactive, + COUNT(*) FILTER (WHERE type = 'image')::int AS image, + COUNT(*) FILTER (WHERE type = 'video')::int AS video, + COUNT(*) FILTER (WHERE type = 'text')::int AS text, + COUNT(*) FILTER ( + WHERE is_active = true + AND (COALESCE(default_api_url, '') = '' OR COALESCE(default_model, '') = '') + )::int AS incomplete + FROM api_providers + `), + safeQuery(client, 'model recommendation summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE is_active = true)::int AS active + FROM model_recommendations + `), + safeQuery(client, 'user api key summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE is_active = true)::int AS active + FROM user_api_keys + `), + safeQuery(client, 'announcement summary', ` + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER ( + WHERE is_active = true + AND (starts_at IS NULL OR starts_at <= NOW()) + AND (expires_at IS NULL OR expires_at >= NOW()) + )::int AS active, + COUNT(*) FILTER (WHERE is_active = true AND starts_at > NOW())::int AS scheduled, + COUNT(*) FILTER (WHERE expires_at < NOW())::int AS expired + FROM announcements + `), + ]); + + const platform = firstRow(platformResult); + const users = firstRow(userResult); + const works = firstRow(workResult); + const orderRevenue = firstRow(orderRevenueResult); + const storage = firstRow(storageResult); + const logs = firstRow(logResult); + const providers = firstRow(providerResult); + const recommendations = firstRow(recommendationResult); + const userApiKeys = firstRow(userApiKeyResult); + const announcements = firstRow(announcementResult); + const taskRows = taskStatusResult.rows; + const orderRows = orderStatusResult.rows; + + const totalTasks = taskRows.reduce((sum, row) => sum + numberValue(row.count), 0); + const totalOrders = orderRows.reduce((sum, row) => sum + numberValue(row.count), 0); + const totalWorks = numberValue(works.total); + + return NextResponse.json({ + generatedAt: new Date().toISOString(), + platform: { + totalVisits: numberValue(platform.total_visits), + databaseTime: platform.database_time || null, + }, + users: { + total: numberValue(users.total), + active: numberValue(users.active), + disabled: numberValue(users.disabled), + admins: numberValue(users.admins), + members: numberValue(users.members), + created7d: numberValue(users.created_7d), + }, + works: { + total: totalWorks, + public: numberValue(works.public), + private: numberValue(works.private), + completed: numberValue(works.completed), + failed: numberValue(works.failed), + withResultUrl: numberValue(works.with_result_url), + created7d: numberValue(works.created_7d), + resultUrlCoverage: totalWorks > 0 ? numberValue(works.with_result_url) / totalWorks : 1, + byType: { + text2img: numberValue(works.text2img), + img2img: numberValue(works.img2img), + text2video: numberValue(works.text2video), + img2video: numberValue(works.img2video), + }, + }, + tasks: { + total: totalTasks, + queued: statusCount(taskRows, 'queued'), + running: statusCount(taskRows, 'running'), + succeeded: statusCount(taskRows, 'succeeded'), + failed: statusCount(taskRows, 'failed'), + latest: latestTaskResult.rows.map(row => ({ + id: String(row.id || ''), + type: String(row.type || ''), + status: String(row.status || ''), + error: row.error ? String(row.error) : null, + createdAt: row.created_at || null, + updatedAt: row.updated_at || null, + })), + }, + orders: { + total: totalOrders, + pending: statusCount(orderRows, 'pending'), + paid: statusCount(orderRows, 'paid'), + cancelled: statusCount(orderRows, 'cancelled'), + refunded: statusCount(orderRows, 'refunded'), + paidRevenue: numberValue(orderRevenue.paid_revenue), + paidRevenue7d: numberValue(orderRevenue.paid_revenue_7d), + latest: latestOrderResult.rows.map(row => ({ + id: String(row.id || ''), + orderNo: String(row.order_no || ''), + productName: String(row.product_name || ''), + amount: numberValue(row.amount), + status: String(row.status || ''), + createdAt: row.created_at || null, + })), + }, + providers: { + total: numberValue(providers.total), + active: numberValue(providers.active), + inactive: numberValue(providers.inactive), + image: numberValue(providers.image), + video: numberValue(providers.video), + text: numberValue(providers.text), + incomplete: numberValue(providers.incomplete), + recommendationsTotal: numberValue(recommendations.total), + recommendationsActive: numberValue(recommendations.active), + userApiKeysTotal: numberValue(userApiKeys.total), + userApiKeysActive: numberValue(userApiKeys.active), + }, + announcements: { + total: numberValue(announcements.total), + active: numberValue(announcements.active), + scheduled: numberValue(announcements.scheduled), + expired: numberValue(announcements.expired), + }, + system: { + apiHealth: true, + databaseHealth: true, + storageHealth: Boolean(process.env.LOCAL_STORAGE_DIR), + storageDirConfigured: Boolean(process.env.LOCAL_STORAGE_DIR), + worksPersisted: numberValue(storage.persisted), + worksTotal: numberValue(storage.total), + logsTotal: numberValue(logs.total), + logsErrors: numberValue(logs.errors), + logsCreated24h: numberValue(logs.created_24h), + }, + }); + } catch (error) { + console.error('[admin/dashboard] GET error:', error); + return NextResponse.json({ error: '获取仪表盘数据失败' }, { status: 500 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/data-export/route.ts b/src/app/api/admin/data-export/route.ts new file mode 100644 index 0000000..dda27a7 --- /dev/null +++ b/src/app/api/admin/data-export/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const client = await getDbClient(); + try { + const data: Record = {}; + + const tables = [ + 'profiles', + 'works', + 'credit_transactions', + 'orders', + 'user_api_keys', + 'system_api_configs', + 'payment_methods', + 'work_likes', + 'announcements', + ]; + + for (const table of tables) { + try { + const result = await client.query(`SELECT * FROM ${table} ORDER BY created_at ASC`); + data[table] = result.rows || []; + } catch { + data[table] = []; + } + } + + try { + const result = await client.query('SELECT * FROM site_config'); + data.site_config = result.rows || []; + } catch { data.site_config = []; } + + try { + const result = await client.query('SELECT * FROM site_stats'); + data.site_stats = result.rows || []; + } catch { data.site_stats = []; } + + try { + const result = await client.query('SELECT id, email, created_at, raw_user_meta_data, password_hash FROM auth.users'); + data.auth_users = result.rows || []; + } catch { data.auth_users = []; } + + const exportData = { + _meta: { + version: '1.0', + platform: 'miaojing', + exported_at: new Date().toISOString(), + tables: Object.keys(data), + counts: Object.fromEntries(Object.entries(data).map(([k, v]) => [k, v.length])), + }, + data, + }; + + return NextResponse.json(exportData); + } finally { + client.release(); + } + } catch (err) { + console.error('[data-export] Error:', err); + return NextResponse.json({ error: err instanceof Error ? err.message : '导出失败' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/data-import/route.ts b/src/app/api/admin/data-import/route.ts new file mode 100644 index 0000000..0973569 --- /dev/null +++ b/src/app/api/admin/data-import/route.ts @@ -0,0 +1,584 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { localStorage } from '@/lib/local-storage'; +import { encryptSecret, previewSecret } from '@/lib/server-crypto'; +import { getDbClient } from '@/storage/database/local-db'; + +interface ImportMeta { + version: string; + platform: string; + exported_at: string; + tables: string[]; + counts: Record; +} + +interface ImportPayload { + _meta: ImportMeta; + data: Record; + options?: { + skipAuth?: boolean; + }; +} + +const MAX_ROWS_PER_TABLE = 5000; +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const UUID_ID_TABLES = new Set([ + 'auth.users', + 'profiles', + 'announcements', + 'works', + 'credit_transactions', + 'orders', + 'user_api_keys', + 'system_api_configs', + 'work_likes', +]); + +const TABLE_COLUMNS: Record = { + profiles: ['id', 'email', 'nickname', 'avatar_url', 'phone', 'role', 'membership_tier', 'membership_expires_at', 'credits_balance', 'daily_quota_used', 'daily_quota_limit', 'is_active', 'preferred_theme', 'created_at', 'updated_at'], + site_config: ['id', 'site_name', 'site_tab_title', 'site_description', 'site_keywords', 'logo_url', 'favicon_url', 'announcement', 'membership_enabled', 'terms_of_service', 'privacy_policy', 'about_us', 'help_center', 'filing_info', 'filing_url', 'public_security_filing_info', 'public_security_filing_url', 'updated_at'], + site_stats: ['id', 'total_visits', 'total_users', 'total_generations', 'updated_at'], + announcements: ['id', 'title', 'content', 'type', 'is_active', 'starts_at', 'expires_at', 'created_at', 'updated_at'], + works: ['id', 'user_id', 'title', 'type', 'prompt', 'negative_prompt', 'params', 'result_url', 'thumbnail_url', 'width', 'height', 'duration', 'status', 'is_public', 'likes_count', 'views_count', 'created_at', 'updated_at'], + credit_transactions: ['id', 'user_id', 'amount', 'balance_after', 'type', 'description', 'related_work_id', 'created_at'], + orders: ['id', 'user_id', 'order_no', 'product_type', 'product_name', 'amount', 'credits_amount', 'status', 'payment_method', 'paid_at', 'created_at', 'updated_at'], + user_api_keys: ['id', 'user_id', 'provider', 'supplier_name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'is_active', 'created_at', 'updated_at'], + system_api_configs: ['id', 'provider', 'name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'credits_per_use', 'is_active', 'sort_order', 'created_at', 'updated_at'], + payment_methods: ['id', 'type', 'name', 'is_active', 'public_config', 'secret_config_encrypted', 'secret_config_preview', 'created_at', 'updated_at'], + work_likes: ['id', 'user_id', 'work_id', 'created_at'], +}; + +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; +const AUTH_USER_COLUMNS = ['id', 'email', 'created_at', 'raw_user_meta_data', 'password_hash']; + +const CONFLICT_COLUMNS: Record = { + 'auth.users': ['id'], + profiles: ['id'], + site_config: ['id'], + site_stats: ['id'], + announcements: ['id'], + works: ['id'], + credit_transactions: ['id'], + orders: ['id'], + user_api_keys: ['id'], + system_api_configs: ['id'], + payment_methods: ['id'], + work_likes: ['id'], +}; + +type ImportResult = { imported: number; skipped: number; errors: string[] }; + +type ImportContext = { + userIdMap: Map; + workIdMap: Map; + emailUserIdMap: Map; + apiKeyIdMap: Map; + apiKeyOwnerIdMap: Map; + columnCache: Map>; +}; + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body: ImportPayload = await request.json(); + const { _meta, data } = body; + const skipAuth = body.options?.skipAuth === true; + + if (!_meta || _meta.platform !== 'miaojing' || !data || typeof data !== 'object') { + return NextResponse.json({ error: '无效的导入文件:格式不匹配' }, { status: 400 }); + } + + const client = await getDbClient(); + const result: Record = {}; + + try { + const context = await buildImportContext(client, data); + + if (!skipAuth && Array.isArray(data.auth_users)) { + result.auth_users = await importRows(client, 'auth.users', AUTH_USER_COLUMNS, data.auth_users, context); + } else { + result.auth_users = { + imported: 0, + skipped: Array.isArray(data.auth_users) ? data.auth_users.length : 0, + errors: skipAuth ? ['已按选项跳过认证账号导入'] : [], + }; + } + + for (const [table, allowedColumns] of Object.entries(TABLE_COLUMNS)) { + const rows = data[table]; + result[table] = await importRows(client, table, allowedColumns, Array.isArray(rows) ? rows : [], context); + } + + return NextResponse.json({ success: true, message: '数据导入完成', details: result, meta: _meta }); + } finally { + client.release(); + } + } catch (err) { + console.error('[data-import] Error:', err instanceof Error ? err.message : err); + return NextResponse.json({ error: err instanceof Error ? err.message : '导入失败' }, { status: 500 }); + } +} + +async function importRows( + client: Awaited>, + table: string, + allowedColumns: string[], + rows: unknown[], + context: ImportContext, +): Promise { + if (rows.length > MAX_ROWS_PER_TABLE) { + return { imported: 0, skipped: rows.length, errors: [`${table}: 单表最多允许导入 ${MAX_ROWS_PER_TABLE} 行`] }; + } + + let imported = 0; + let skipped = 0; + const errors: string[] = []; + const existingColumns = await getExistingColumns(client, table, context); + const effectiveAllowedColumns = allowedColumns.filter(col => existingColumns.has(col)); + + for (const rawRow of rows) { + const row = await normalizeImportRow(table, rawRow as Record, context); + const cols = Object.keys(row).filter(col => effectiveAllowedColumns.includes(col)); + if (!cols.includes('id') || cols.length === 0) { + skipped++; + errors.push(`${table}: 缺少 id 或没有允许导入的字段`); + continue; + } + + try { + const vals = cols.map(col => row[col]); + const placeholders = cols.map((_, i) => `$${i + 1}`).join(', '); + const conflictCols = CONFLICT_COLUMNS[table] || ['id']; + + const mergeAssignments = getMergeAssignments(table, cols); + const conflictAction = mergeAssignments.length > 0 + ? `DO UPDATE SET ${mergeAssignments.join(', ')}` + : 'DO NOTHING'; + + const insertResult = await client.query( + `INSERT INTO ${table} AS target (${cols.join(', ')}) VALUES (${placeholders}) ON CONFLICT (${conflictCols.join(', ')}) ${conflictAction}`, + vals, + ); + if ((insertResult.rowCount || 0) > 0) { + imported++; + } else { + skipped++; + } + } catch (e) { + skipped++; + errors.push(`${table}: ${e instanceof Error ? e.message : 'unknown error'}`); + } + } + + return { imported, skipped, errors }; +} + +async function buildImportContext( + client: Awaited>, + data: Record, +): Promise { + const userIdMap = new Map(); + const workIdMap = new Map(); + const emailUserIdMap = new Map(); + const apiKeyIdMap = new Map(); + const apiKeyOwnerIdMap = new Map(); + + const profileRows = Array.isArray(data.profiles) ? data.profiles : []; + const authRows = Array.isArray(data.auth_users) ? data.auth_users : []; + const profileEmails = new Map(); + + for (const raw of profileRows) { + const row = raw as Record; + seedUuidMap(userIdMap, row.id); + if (typeof row.id === 'string' && typeof row.email === 'string' && row.email.trim()) { + const email = row.email.trim().toLowerCase(); + profileEmails.set(email, row.id); + emailUserIdMap.set(email, userIdMap.get(row.id) || row.id); + } + } + for (const raw of authRows) { + const row = raw as Record; + seedUuidMap(userIdMap, row.id); + if (typeof row.id === 'string' && typeof row.email === 'string' && row.email.trim() && !profileEmails.has(row.email.trim().toLowerCase())) { + const email = row.email.trim().toLowerCase(); + profileEmails.set(email, row.id); + emailUserIdMap.set(email, userIdMap.get(row.id) || row.id); + } + } + + if (profileEmails.size > 0) { + const emails = [...profileEmails.keys()]; + const existing = await client.query( + 'SELECT id, lower(email) AS email FROM profiles WHERE lower(email) = ANY($1)', + [emails], + ); + for (const row of existing.rows) { + const importedId = profileEmails.get(row.email); + if (importedId && importedId !== row.id) { + userIdMap.set(importedId, row.id); + emailUserIdMap.set(row.email, row.id); + } + } + } + + for (const [email, importedId] of profileEmails.entries()) { + emailUserIdMap.set(email, userIdMap.get(importedId) || importedId); + } + + const apiKeyRows = Array.isArray(data.user_api_keys) ? data.user_api_keys : []; + for (const raw of apiKeyRows) { + const row = raw as Record; + const oldId = typeof row.id === 'string' && row.id.trim() ? row.id.trim() : ''; + if (oldId) { + apiKeyIdMap.set(oldId, isUuid(oldId) ? oldId : crypto.randomUUID()); + } + const ownerId = findImportedWorkUserId(row); + const ownerByEmail = findUserIdByEmail(row, { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map() }); + const mappedOwnerId = ownerId + ? (userIdMap.get(ownerId) || ownerId) + : ownerByEmail; + if (oldId && mappedOwnerId) { + apiKeyOwnerIdMap.set(oldId, mappedOwnerId); + } + } + + const works = Array.isArray(data.works) ? data.works : []; + const workUrls = new Map(); + for (const raw of works) { + const row = raw as Record; + seedUuidMap(workIdMap, row.id); + if (typeof row.id === 'string' && typeof row.result_url === 'string' && row.result_url.trim() && !isDataUrl(row.result_url)) { + workUrls.set(row.result_url.trim(), row.id); + } + } + if (workUrls.size > 0) { + const existing = await client.query( + 'SELECT id, result_url FROM works WHERE result_url = ANY($1)', + [[...workUrls.keys()]], + ); + for (const row of existing.rows) { + const importedId = workUrls.get(row.result_url); + if (importedId && importedId !== row.id) { + workIdMap.set(importedId, row.id); + } + } + } + + return { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map() }; +} + +async function normalizeImportRow(table: string, row: Record, context: ImportContext): Promise> { + const next = { ...row }; + + if (typeof next.user_id === 'string' && context.userIdMap.has(next.user_id)) { + next.user_id = context.userIdMap.get(next.user_id); + } + if ((!next.user_id || next.user_id === SYSTEM_USER_ID) && findUserIdByEmail(next, context)) { + next.user_id = findUserIdByEmail(next, context); + } + if (typeof next.related_work_id === 'string' && context.workIdMap.has(next.related_work_id)) { + next.related_work_id = context.workIdMap.get(next.related_work_id); + } + if (typeof next.work_id === 'string' && context.workIdMap.has(next.work_id)) { + next.work_id = context.workIdMap.get(next.work_id); + } + + if (table === 'auth.users' || table === 'profiles') { + const currentId = typeof next.id === 'string' ? next.id : ''; + if (currentId && context.userIdMap.has(currentId)) { + next.id = context.userIdMap.get(currentId); + } + } + + if (table === 'user_api_keys') { + const currentId = typeof next.id === 'string' ? next.id : ''; + if (currentId && context.apiKeyIdMap.has(currentId)) { + next.id = context.apiKeyIdMap.get(currentId); + } + const importedUserId = findImportedWorkUserId(next); + const emailUserId = findUserIdByEmail(next, context); + if (importedUserId || emailUserId) { + next.user_id = importedUserId + ? (context.userIdMap.get(importedUserId) || importedUserId) + : emailUserId; + } + } + + if (table === 'works') { + const currentId = typeof next.id === 'string' ? next.id : ''; + if (currentId && context.workIdMap.has(currentId)) { + next.id = context.workIdMap.get(currentId); + } + const importedUserId = findImportedWorkUserId(next) || findUserIdByEmail(next, context) || findUserIdByCustomModel(next, context); + if (importedUserId) { + next.user_id = context.userIdMap.get(importedUserId) || importedUserId; + } + if (typeof next.result_url === 'string') { + next.result_url = await persistImportMedia(next.result_url, getWorkMediaFolder(next.type, 'results')); + } + if (typeof next.thumbnail_url === 'string') { + next.thumbnail_url = await persistImportMedia(next.thumbnail_url, 'imported/works/thumbnails'); + } + if (next.params && typeof next.params === 'object') { + next.params = await sanitizeImportMedia(next.params, 'imported/works/references'); + remapCustomModelId(next.params as Record, context); + if ((!next.user_id || next.user_id === SYSTEM_USER_ID) && findUserIdByCustomModel(next, context)) { + next.user_id = findUserIdByCustomModel(next, context); + } + } + } + + if (table === 'user_api_keys') { + const rawEncrypted = typeof next.api_key_encrypted === 'string' ? next.api_key_encrypted.trim() : ''; + const rawApiKey = typeof next.apiKey === 'string' ? next.apiKey.trim() : ''; + const secret = rawApiKey || rawEncrypted; + if (secret) { + next.api_key_encrypted = encryptSecret(secret); + next.api_key_preview = typeof next.api_key_preview === 'string' && next.api_key_preview + ? next.api_key_preview + : previewSecret(secret); + } + } + + if (UUID_ID_TABLES.has(table)) { + const currentId = typeof next.id === 'string' ? next.id : ''; + if (!isUuid(currentId)) { + next.id = crypto.randomUUID(); + } + } + + return next; +} + +function findImportedWorkUserId(row: Record): string | null { + const directKeys = ['user_id', 'userId', 'publisher_id', 'publisherId', 'owner_id', 'ownerId', 'created_by', 'createdBy']; + for (const key of directKeys) { + const value = row[key]; + if (typeof value === 'string' && value.trim() && value !== 'anonymous' && value !== '00000000-0000-0000-0000-000000000000') { + return value.trim(); + } + } + + const params = row.params && typeof row.params === 'object' ? row.params as Record : null; + if (!params) return null; + for (const key of directKeys) { + const value = params[key]; + if (typeof value === 'string' && value.trim() && value !== 'anonymous' && value !== '00000000-0000-0000-0000-000000000000') { + return value.trim(); + } + } + return null; +} + +function findUserIdByEmail(row: Record, context: ImportContext): string | null { + const directKeys = ['email', 'user_email', 'userEmail', 'publisher_email', 'publisherEmail', 'owner_email', 'ownerEmail']; + for (const key of directKeys) { + const value = row[key]; + if (typeof value === 'string' && value.trim()) { + const mapped = context.emailUserIdMap.get(value.trim().toLowerCase()); + if (mapped) return mapped; + } + } + + const params = row.params && typeof row.params === 'object' ? row.params as Record : null; + if (!params) return null; + for (const key of directKeys) { + const value = params[key]; + if (typeof value === 'string' && value.trim()) { + const mapped = context.emailUserIdMap.get(value.trim().toLowerCase()); + if (mapped) return mapped; + } + } + return null; +} + +function findUserIdByCustomModel(row: Record, context: ImportContext): string | null { + const params = row.params && typeof row.params === 'object' ? row.params as Record : null; + const model = typeof params?.model === 'string' + ? params.model + : typeof row.model === 'string' + ? row.model + : ''; + if (!model.startsWith('custom:')) return null; + const oldId = model.slice('custom:'.length); + return context.apiKeyOwnerIdMap.get(oldId) || null; +} + +function remapCustomModelId(params: Record, context: ImportContext): void { + const model = typeof params.model === 'string' ? params.model : ''; + if (!model.startsWith('custom:')) return; + const oldId = model.slice('custom:'.length); + const newId = context.apiKeyIdMap.get(oldId); + if (newId) { + params.model = `custom:${newId}`; + } +} + +function getMergeAssignments(table: string, cols: string[]): string[] { + const has = (column: string) => cols.includes(column); + const assignments: string[] = []; + + if (table === 'auth.users') { + if (has('email')) assignments.push(`email = COALESCE(NULLIF(target.email, ''), EXCLUDED.email)`); + if (has('raw_user_meta_data')) assignments.push(`raw_user_meta_data = COALESCE(target.raw_user_meta_data, EXCLUDED.raw_user_meta_data)`); + if (has('password_hash')) assignments.push(`password_hash = COALESCE(NULLIF(target.password_hash, ''), EXCLUDED.password_hash)`); + return assignments; + } + + if (table === 'profiles') { + if (has('email')) assignments.push(`email = COALESCE(NULLIF(target.email, ''), EXCLUDED.email)`); + if (has('nickname')) assignments.push(`nickname = COALESCE(NULLIF(target.nickname, ''), EXCLUDED.nickname)`); + if (has('avatar_url')) assignments.push(`avatar_url = COALESCE(NULLIF(target.avatar_url, ''), EXCLUDED.avatar_url)`); + if (has('phone')) assignments.push(`phone = COALESCE(NULLIF(target.phone, ''), EXCLUDED.phone)`); + if (has('role')) assignments.push(`role = CASE WHEN target.role = 'admin' THEN target.role ELSE COALESCE(NULLIF(target.role, ''), EXCLUDED.role) END`); + if (has('membership_tier')) assignments.push(`membership_tier = COALESCE(NULLIF(target.membership_tier, ''), EXCLUDED.membership_tier)`); + if (has('membership_expires_at')) assignments.push(`membership_expires_at = COALESCE(target.membership_expires_at, EXCLUDED.membership_expires_at)`); + if (has('credits_balance')) assignments.push(`credits_balance = COALESCE(target.credits_balance, EXCLUDED.credits_balance)`); + if (has('daily_quota_limit')) assignments.push(`daily_quota_limit = COALESCE(target.daily_quota_limit, EXCLUDED.daily_quota_limit)`); + if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); + if (has('preferred_theme')) assignments.push(`preferred_theme = CASE WHEN EXCLUDED.preferred_theme IN ('dark', 'light') THEN EXCLUDED.preferred_theme ELSE target.preferred_theme END`); + if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); + return assignments; + } + + if (table === 'works') { + if (has('user_id')) { + assignments.push(`user_id = CASE WHEN (target.user_id IS NULL OR target.user_id = '${SYSTEM_USER_ID}'::uuid) AND EXCLUDED.user_id IS NOT NULL AND EXCLUDED.user_id <> '${SYSTEM_USER_ID}'::uuid THEN EXCLUDED.user_id ELSE target.user_id END`); + } + if (has('params')) assignments.push(`params = CASE WHEN target.params IS NULL OR target.params = '{}'::jsonb THEN EXCLUDED.params ELSE target.params END`); + if (has('thumbnail_url')) assignments.push(`thumbnail_url = COALESCE(NULLIF(target.thumbnail_url, ''), EXCLUDED.thumbnail_url)`); + if (has('width')) assignments.push(`width = COALESCE(target.width, EXCLUDED.width)`); + if (has('height')) assignments.push(`height = COALESCE(target.height, EXCLUDED.height)`); + if (has('duration')) assignments.push(`duration = COALESCE(target.duration, EXCLUDED.duration)`); + if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); + return assignments; + } + + if (table === 'user_api_keys') { + if (has('user_id')) assignments.push(`user_id = COALESCE(target.user_id, EXCLUDED.user_id)`); + if (has('provider')) assignments.push(`provider = COALESCE(NULLIF(target.provider, ''), EXCLUDED.provider)`); + if (has('supplier_name')) assignments.push(`supplier_name = COALESCE(NULLIF(target.supplier_name, ''), EXCLUDED.supplier_name)`); + if (has('api_url')) assignments.push(`api_url = COALESCE(NULLIF(target.api_url, ''), EXCLUDED.api_url)`); + if (has('model_name')) assignments.push(`model_name = COALESCE(NULLIF(target.model_name, ''), EXCLUDED.model_name)`); + if (has('note')) assignments.push(`note = COALESCE(NULLIF(target.note, ''), EXCLUDED.note)`); + if (has('api_key_encrypted')) assignments.push(`api_key_encrypted = COALESCE(NULLIF(target.api_key_encrypted, ''), EXCLUDED.api_key_encrypted)`); + if (has('api_key_preview')) assignments.push(`api_key_preview = COALESCE(NULLIF(target.api_key_preview, ''), EXCLUDED.api_key_preview)`); + if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); + if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); + if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); + return assignments; + } + + if (table === 'system_api_configs') { + if (has('provider')) assignments.push(`provider = COALESCE(NULLIF(target.provider, ''), EXCLUDED.provider)`); + if (has('name')) assignments.push(`name = COALESCE(NULLIF(target.name, ''), EXCLUDED.name)`); + if (has('api_url')) assignments.push(`api_url = COALESCE(NULLIF(target.api_url, ''), EXCLUDED.api_url)`); + if (has('model_name')) assignments.push(`model_name = COALESCE(NULLIF(target.model_name, ''), EXCLUDED.model_name)`); + if (has('note')) assignments.push(`note = COALESCE(NULLIF(target.note, ''), EXCLUDED.note)`); + if (has('api_key_encrypted')) assignments.push(`api_key_encrypted = COALESCE(NULLIF(target.api_key_encrypted, ''), EXCLUDED.api_key_encrypted)`); + if (has('api_key_preview')) assignments.push(`api_key_preview = COALESCE(NULLIF(target.api_key_preview, ''), EXCLUDED.api_key_preview)`); + if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); + if (has('credits_per_use')) assignments.push(`credits_per_use = COALESCE(target.credits_per_use, EXCLUDED.credits_per_use)`); + if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); + if (has('sort_order')) assignments.push(`sort_order = COALESCE(target.sort_order, EXCLUDED.sort_order)`); + if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); + return assignments; + } + + if (table === 'payment_methods') { + if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); + if (has('name')) assignments.push(`name = COALESCE(NULLIF(target.name, ''), EXCLUDED.name)`); + if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); + if (has('public_config')) assignments.push(`public_config = COALESCE(target.public_config, EXCLUDED.public_config)`); + if (has('secret_config_encrypted')) assignments.push(`secret_config_encrypted = COALESCE(target.secret_config_encrypted, EXCLUDED.secret_config_encrypted)`); + if (has('secret_config_preview')) assignments.push(`secret_config_preview = COALESCE(target.secret_config_preview, EXCLUDED.secret_config_preview)`); + if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); + return assignments; + } + + return assignments; +} + +async function getExistingColumns( + client: Awaited>, + table: string, + context: ImportContext, +): Promise> { + const cached = context.columnCache.get(table); + if (cached) return cached; + + const [schemaName, tableName] = table.includes('.') ? table.split('.', 2) : ['public', table]; + const result = await client.query( + 'SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2', + [schemaName, tableName], + ); + const columns = new Set((result.rows || []).map((row: Record) => String(row.column_name))); + context.columnCache.set(table, columns); + return columns; +} + +function seedUuidMap(map: Map, value: unknown): void { + if (typeof value === 'string' && value && !isUuid(value) && !map.has(value)) { + map.set(value, crypto.randomUUID()); + } +} + +function isUuid(value: unknown): value is string { + return typeof value === 'string' && UUID_REGEX.test(value); +} + +function isDataUrl(value: unknown): boolean { + return typeof value === 'string' && /^data:[^,]+,/i.test(value); +} + +function getWorkMediaFolder(type: unknown, kind: string): string { + const text = typeof type === 'string' ? type.toLowerCase() : ''; + const media = text.includes('video') ? 'videos' : 'images'; + return `imported/works/${kind}/${media}`; +} + +function extensionFromMime(mime: string): string { + const normalized = mime.toLowerCase(); + if (normalized.includes('png')) return 'png'; + if (normalized.includes('jpeg') || normalized.includes('jpg')) return 'jpg'; + if (normalized.includes('webp')) return 'webp'; + if (normalized.includes('gif')) return 'gif'; + if (normalized.includes('mp4')) return 'mp4'; + if (normalized.includes('webm')) return 'webm'; + return 'bin'; +} + +async function persistImportMedia(value: string, folder: string): Promise { + if (!isDataUrl(value)) return value; + + const match = value.match(/^data:([^;,]+)?(;base64)?,([\s\S]*)$/i); + if (!match) return value; + + const mime = match[1] || 'application/octet-stream'; + const isBase64 = Boolean(match[2]); + const payload = match[3] || ''; + const buffer = isBase64 ? Buffer.from(payload, 'base64') : Buffer.from(decodeURIComponent(payload)); + const ext = extensionFromMime(mime); + const key = `${folder}/${Date.now()}-${crypto.randomUUID()}.${ext}`; + const savedKey = await localStorage.uploadFile({ fileContent: buffer, fileName: key, contentType: mime }); + return localStorage.generatePresignedUrl({ key: savedKey, expireTime: 2592000 }); +} + +async function sanitizeImportMedia(value: unknown, folder: string): Promise { + if (typeof value === 'string') { + return persistImportMedia(value, folder); + } + if (Array.isArray(value)) { + return Promise.all(value.map(item => sanitizeImportMedia(item, folder))); + } + if (value && typeof value === 'object') { + const output: Record = {}; + for (const [key, nested] of Object.entries(value as Record)) { + output[key] = await sanitizeImportMedia(nested, folder); + } + return output; + } + return value; +} diff --git a/src/app/api/admin/email-recipients/route.ts b/src/app/api/admin/email-recipients/route.ts new file mode 100644 index 0000000..7177bf2 --- /dev/null +++ b/src/app/api/admin/email-recipients/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { isValidEmail, normalizeEmail } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +function mapRecipient(row: Record) { + const email = normalizeEmail(row.email); + if (!isValidEmail(email)) return null; + return { + id: String(row.id), + email, + nickname: typeof row.nickname === 'string' && row.nickname.trim() ? row.nickname.trim() : email.split('@')[0], + phone: typeof row.phone === 'string' ? row.phone : null, + avatarUrl: typeof row.avatar_url === 'string' ? row.avatar_url : null, + emailVerified: row.email_verified === true, + }; +} + +export async function GET(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const { searchParams } = new URL(request.url); + const q = (searchParams.get('q') || '').trim().toLowerCase().slice(0, 80); + const limit = Math.min(80, Math.max(1, Number(searchParams.get('limit') || 30))); + + const client = await getDbClient(); + try { + const params: unknown[] = []; + let filter = ` + WHERE COALESCE(role, 'user') NOT IN ('admin', 'enterprise_admin') + AND COALESCE(is_active, true) = true + AND COALESCE(email, '') <> '' + `; + + if (q) { + params.push(`%${q}%`); + filter += ` + AND ( + LOWER(email) LIKE $${params.length} + OR LOWER(COALESCE(nickname, '')) LIKE $${params.length} + OR COALESCE(phone, '') LIKE $${params.length} + ) + `; + } + + const result = await client.query( + `SELECT id, email, nickname, phone, avatar_url, email_verified + FROM profiles + ${filter} + ORDER BY created_at DESC + LIMIT ${limit}`, + params, + ); + + const countResult = await client.query( + `SELECT COUNT(*)::int AS count + FROM profiles + WHERE COALESCE(role, 'user') NOT IN ('admin', 'enterprise_admin') + AND COALESCE(is_active, true) = true + AND COALESCE(email, '') <> ''`, + ); + + const users = result.rows + .map(mapRecipient) + .filter((item): item is NonNullable> => Boolean(item)); + + return NextResponse.json({ + users, + total: Number(countResult.rows[0]?.count || 0), + }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/email-settings/route.ts b/src/app/api/admin/email-settings/route.ts new file mode 100644 index 0000000..9ebb288 --- /dev/null +++ b/src/app/api/admin/email-settings/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { + getEmailSettings, + getRequestBaseUrl, + publicEmailSettings, + renderEmailTemplate, + saveEmailSettings, + sendTemplatedEmail, +} from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +export async function GET(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const client = await getDbClient(); + try { + const settings = await getEmailSettings(client); + const platformUrl = getRequestBaseUrl(request) || settings.appBaseUrl; + const preview = renderEmailTemplate(settings, { + title: '通知邮件模板预览', + intro: '这是一封由管理员发送给用户的通知邮件示例,用于预览全局通用邮件模板效果。', + body: '你可以在后台使用这套模板发送系统公告、功能更新、订单提醒、活动通知和安全提醒。实际发送时,标题、正文、按钮和备注会替换为管理员填写的内容。', + buttonText: '进入妙境', + buttonUrl: platformUrl, + note: '验证码邮件使用独立安全验证模板;管理员通知、管理员邮件和提醒邮件使用这套通用模板。', + templateKind: 'notification', + assetBaseUrl: platformUrl, + }); + return NextResponse.json({ settings: publicEmailSettings(settings), preview }); + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const client = await getDbClient(); + try { + const body = await request.json(); + const settings = await saveEmailSettings(client, body); + return NextResponse.json({ success: true, settings, message: '邮箱配置已保存' }); + } catch (error) { + const message = error instanceof Error ? error.message : '邮箱配置保存失败'; + return NextResponse.json({ error: message }, { status: 400 }); + } finally { + client.release(); + } +} + +export async function POST(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const client = await getDbClient(); + try { + const body = await request.json(); + const to = typeof body.to === 'string' ? body.to.trim() : ''; + if (!to) { + return NextResponse.json({ error: '请填写测试收件邮箱' }, { status: 400 }); + } + await sendTemplatedEmail(client, { + to, + type: 'business', + subject: '【妙境】邮箱配置测试', + title: '邮箱配置测试', + intro: '如果你收到这封邮件,说明自定义域名邮箱 SMTP 配置已生效。', + note: '请同时检查收件箱、垃圾箱,以及 SPF/DKIM/DMARC 解析状态。', + ipAddress: 'admin-test', + assetBaseUrl: getRequestBaseUrl(request) || undefined, + }); + return NextResponse.json({ success: true, message: '测试邮件已发送' }); + } catch (error) { + const message = error instanceof Error ? error.message : '测试邮件发送失败'; + return NextResponse.json({ error: message }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/generation-jobs/route.ts b/src/app/api/admin/generation-jobs/route.ts new file mode 100644 index 0000000..1084fee --- /dev/null +++ b/src/app/api/admin/generation-jobs/route.ts @@ -0,0 +1,123 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; +import { markStaleRunningJobs } from '@/lib/generation-job-worker'; +import { ensureGenerationJobRuntimeSchema } from '@/lib/generation-job-estimates'; +import { writePlatformLog } from '@/lib/platform-logs'; + +const STATUSES = new Set(['queued', 'running', 'succeeded', 'failed']); +const CLEANUP_STATUSES = new Set(['failed', 'succeeded']); + +function intParam(value: string | null, fallback: number, min: number, max: number) { + const parsed = Number.parseInt(value || '', 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, parsed)); +} + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + await markStaleRunningJobs(); + + const { searchParams } = new URL(request.url); + const status = searchParams.get('status') || ''; + const userSearch = (searchParams.get('user') || searchParams.get('userSearch') || '').trim(); + const page = intParam(searchParams.get('page'), 1, 1, 100000); + const pageSize = intParam(searchParams.get('pageSize'), 20, 1, 100); + const offset = (page - 1) * pageSize; + + if (status && !STATUSES.has(status)) { + return NextResponse.json({ error: '任务状态无效' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + await ensureGenerationJobRuntimeSchema(client); + const whereClauses: string[] = []; + const params: unknown[] = []; + if (status) { + params.push(status); + whereClauses.push(`j.status = $${params.length}`); + } + if (userSearch) { + params.push(`%${userSearch.toLowerCase()}%`); + whereClauses.push(`( + j.user_id::text LIKE $${params.length} + OR LOWER(COALESCE(p.email, '')) LIKE $${params.length} + OR LOWER(COALESCE(p.nickname, '')) LIKE $${params.length} + )`); + } + const whereSql = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : ''; + const countResult = await client.query( + `SELECT COUNT(*)::int AS total + FROM generation_jobs j + LEFT JOIN profiles p ON p.id = j.user_id + ${whereSql}`, + params, + ); + const rowsResult = await client.query( + `SELECT j.id, j.user_id, p.email AS user_email, p.nickname AS user_nickname, + j.type, j.status, j.error, j.created_at, j.started_at, j.finished_at, j.updated_at + FROM generation_jobs j + LEFT JOIN profiles p ON p.id = j.user_id + ${whereSql} + ORDER BY j.created_at DESC + LIMIT $${params.length + 1} + OFFSET $${params.length + 2}`, + [...params, pageSize, offset], + ); + + const total = countResult.rows[0]?.total || 0; + return NextResponse.json({ + jobs: rowsResult.rows, + total, + page, + pageSize, + totalPages: Math.max(1, Math.ceil(total / pageSize)), + }); + } finally { + client.release(); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const { searchParams } = new URL(request.url); + const status = searchParams.get('status') || 'failed'; + const olderThanDays = intParam(searchParams.get('olderThanDays'), 7, 0, 3650); + + if (!CLEANUP_STATUSES.has(status)) { + return NextResponse.json( + { error: '只允许清理失败或已完成任务' }, + { status: 400 }, + ); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `DELETE FROM generation_jobs + WHERE status = $1 + AND updated_at < NOW() - ($2::int * INTERVAL '1 day')`, + [status, olderThanDays], + ); + void writePlatformLog({ + type: 'admin', + level: 'warning', + action: 'generation_jobs_cleanup', + message: `管理员清理了${status === 'failed' ? '失败' : '已完成'}生成任务`, + targetType: 'generation_jobs', + metadata: { status, olderThanDays, deleted: result.rowCount || 0 }, + request, + }); + return NextResponse.json({ + success: true, + deleted: result.rowCount || 0, + }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/model-recommendations/route.ts b/src/app/api/admin/model-recommendations/route.ts new file mode 100644 index 0000000..870f5b0 --- /dev/null +++ b/src/app/api/admin/model-recommendations/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +function mapRecommendation(row: Record) { + return { + id: String(row.id), + modelName: String(row.model_name || ''), + displayName: String(row.display_name || row.model_name || ''), + type: String(row.type || 'image'), + providerId: (row.provider_id as string | null) || null, + isActive: row.is_active !== false, + sortOrder: Number(row.sort_order || 0), + }; +} + +async function readBody(request: NextRequest) { + return request.json().catch(() => ({})); +} + +export async function GET() { + const client = await getDbClient(); + try { + const result = await client.query( + `SELECT id, model_name, display_name, type, provider_id, is_active, sort_order + FROM model_recommendations + ORDER BY type ASC, sort_order ASC, model_name ASC` + ); + return NextResponse.json({ recommendations: result.rows.map(mapRecommendation) }); + } finally { + client.release(); + } +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!body.modelName?.trim()) { + return NextResponse.json({ error: '请填写模型名称' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `INSERT INTO model_recommendations (model_name, display_name, type, provider_id, is_active, sort_order) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, model_name, display_name, type, provider_id, is_active, sort_order`, + [ + body.modelName.trim(), + body.displayName?.trim() || body.modelName.trim(), + body.type || 'image', + body.providerId || null, + body.isActive !== false, + Number(body.sortOrder || 0), + ] + ); + return NextResponse.json({ recommendation: mapRecommendation(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!body.id || !body.modelName?.trim()) { + return NextResponse.json({ error: '缺少推荐项 ID 或模型名称' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `UPDATE model_recommendations + SET model_name = $2, display_name = $3, type = $4, provider_id = $5, + is_active = $6, sort_order = $7, updated_at = NOW() + WHERE id = $1 + RETURNING id, model_name, display_name, type, provider_id, is_active, sort_order`, + [ + body.id, + body.modelName.trim(), + body.displayName?.trim() || body.modelName.trim(), + body.type || 'image', + body.providerId || null, + body.isActive !== false, + Number(body.sortOrder || 0), + ] + ); + + if (result.rows.length === 0) { + return NextResponse.json({ error: '推荐模型不存在' }, { status: 404 }); + } + + return NextResponse.json({ recommendation: mapRecommendation(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + const id = body.id || request.nextUrl.searchParams.get('id'); + if (!id) return NextResponse.json({ error: '缺少推荐项 ID' }, { status: 400 }); + + const client = await getDbClient(); + try { + await client.query('DELETE FROM model_recommendations WHERE id = $1', [id]); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/orders/route.ts b/src/app/api/admin/orders/route.ts new file mode 100644 index 0000000..8650c31 --- /dev/null +++ b/src/app/api/admin/orders/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { requireAdmin } from '@/lib/admin-auth'; + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const client = await getDbClient(); + try { + const result = await client.query('SELECT * FROM orders ORDER BY created_at DESC LIMIT 100'); + return NextResponse.json({ orders: result.rows || [] }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/orders] GET error:', err); + return NextResponse.json({ error: '获取订单列表失败' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + const client = await getDbClient(); + try { + const id = crypto.randomUUID(); + const { user_id, order_no, product_type, product_name, amount, credits_amount, status, payment_method } = body; + await client.query( + 'INSERT INTO orders (id, user_id, order_no, product_type, product_name, amount, credits_amount, status, payment_method) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)', + [id, user_id, order_no, product_type, product_name, amount, credits_amount, status || 'pending', payment_method] + ); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/orders] POST error:', err); + return NextResponse.json({ error: '创建订单失败' }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + const { orderId, ...updates } = body; + + if (!orderId) { + return NextResponse.json({ error: '缺少订单ID' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const setClauses: string[] = []; + const params: unknown[] = []; + let paramIdx = 1; + + if (updates.status !== undefined) { setClauses.push(`status = $${paramIdx++}`); params.push(updates.status); } + if (updates.payment_method !== undefined) { setClauses.push(`payment_method = $${paramIdx++}`); params.push(updates.payment_method); } + if (updates.paid_at !== undefined) { setClauses.push(`paid_at = $${paramIdx++}`); params.push(updates.paid_at); } + setClauses.push('updated_at = NOW()'); + + params.push(orderId); + await client.query(`UPDATE orders SET ${setClauses.join(', ')} WHERE id = $${paramIdx}`, params); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/orders] PUT error:', err); + return NextResponse.json({ error: '更新订单失败' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/payment-methods/route.ts b/src/app/api/admin/payment-methods/route.ts new file mode 100644 index 0000000..13730c1 --- /dev/null +++ b/src/app/api/admin/payment-methods/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; +import { listPaymentMethods, savePaymentMethod } from '@/lib/server-payment-config'; + +async function readBody(request: NextRequest) { + return request.json().catch(() => ({})); +} + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const client = await getDbClient(); + try { + return NextResponse.json({ paymentMethods: await listPaymentMethods(client) }); + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (typeof body.id !== 'string' || !body.id.trim()) { + return NextResponse.json({ error: '缺少支付方式 ID' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const paymentMethods = await savePaymentMethod(client, body.id.trim(), { + name: typeof body.name === 'string' ? body.name : undefined, + isActive: typeof body.isActive === 'boolean' ? body.isActive : undefined, + config: body.config && typeof body.config === 'object' ? body.config : undefined, + }); + return NextResponse.json({ paymentMethods }); + } catch (err) { + return NextResponse.json( + { error: err instanceof Error ? err.message : '保存失败' }, + { status: 400 }, + ); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/providers/route.ts b/src/app/api/admin/providers/route.ts new file mode 100644 index 0000000..49a347b --- /dev/null +++ b/src/app/api/admin/providers/route.ts @@ -0,0 +1,121 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +function mapProvider(row: Record) { + return { + id: String(row.id), + name: String(row.name || ''), + defaultApiUrl: String(row.default_api_url || ''), + defaultModel: String(row.default_model || ''), + type: String(row.type || 'image'), + website: (row.website as string | null) || null, + isActive: row.is_active !== false, + sortOrder: Number(row.sort_order || 0), + }; +} + +async function readBody(request: NextRequest) { + return request.json().catch(() => ({})); +} + +export async function GET() { + const client = await getDbClient(); + try { + const result = await client.query( + `SELECT id, name, default_api_url, default_model, type, website, is_active, sort_order + FROM api_providers + ORDER BY sort_order ASC, name ASC` + ); + return NextResponse.json({ providers: result.rows.map(mapProvider) }); + } finally { + client.release(); + } +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!body.name?.trim()) { + return NextResponse.json({ error: '请填写供应商名称' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `INSERT INTO api_providers (name, default_api_url, default_model, type, website, is_active, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, name, default_api_url, default_model, type, website, is_active, sort_order`, + [ + body.name.trim(), + body.defaultApiUrl?.trim() || '', + body.defaultModel?.trim() || '', + body.type || 'image', + body.website?.trim() || null, + body.isActive !== false, + Number(body.sortOrder || 0), + ] + ); + return NextResponse.json({ provider: mapProvider(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!body.id || !body.name?.trim()) { + return NextResponse.json({ error: '缺少供应商 ID 或名称' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `UPDATE api_providers + SET name = $2, default_api_url = $3, default_model = $4, type = $5, website = $6, + is_active = $7, sort_order = $8, updated_at = NOW() + WHERE id = $1 + RETURNING id, name, default_api_url, default_model, type, website, is_active, sort_order`, + [ + body.id, + body.name.trim(), + body.defaultApiUrl?.trim() || '', + body.defaultModel?.trim() || '', + body.type || 'image', + body.website?.trim() || null, + body.isActive !== false, + Number(body.sortOrder || 0), + ] + ); + + if (result.rows.length === 0) { + return NextResponse.json({ error: '供应商不存在' }, { status: 404 }); + } + + return NextResponse.json({ provider: mapProvider(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + const id = body.id || request.nextUrl.searchParams.get('id'); + if (!id) return NextResponse.json({ error: '缺少供应商 ID' }, { status: 400 }); + + const client = await getDbClient(); + try { + await client.query('DELETE FROM api_providers WHERE id = $1', [id]); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/send-email/route.ts b/src/app/api/admin/send-email/route.ts new file mode 100644 index 0000000..cc193c3 --- /dev/null +++ b/src/app/api/admin/send-email/route.ts @@ -0,0 +1,147 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getRequestBaseUrl, isValidEmail, normalizeEmail, sendTemplatedEmail } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +const MAX_TARGETED_RECIPIENTS = 200; +const MAX_BROADCAST_RECIPIENTS = 5000; +type AdminMailKind = 'notification' | 'admin'; + +function normalizeMailKind(value: unknown): AdminMailKind { + return value === 'admin' ? 'admin' : 'notification'; +} + +function normalizeIdList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return [...new Set(value + .filter((item): item is string => typeof item === 'string') + .map(item => item.trim()) + .filter(item => /^[0-9a-fA-F-]{36}$/.test(item)))]; +} + +function normalizeEmailList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return [...new Set(value + .map(normalizeEmail) + .filter(isValidEmail))]; +} + +async function loadRecipients(client: Awaited>, body: Record) { + const mode = body.mode === 'all' ? 'all' : 'selected'; + + if (mode === 'all') { + const result = await client.query( + `SELECT id, email + FROM profiles + WHERE COALESCE(role, 'user') NOT IN ('admin', 'enterprise_admin') + AND COALESCE(is_active, true) = true + AND COALESCE(email, '') <> '' + ORDER BY created_at ASC + LIMIT $1`, + [MAX_BROADCAST_RECIPIENTS], + ); + return result.rows + .map(row => ({ id: String(row.id), email: normalizeEmail(row.email) })) + .filter(row => isValidEmail(row.email)); + } + + const userIds = normalizeIdList(body.userIds); + const emails = normalizeEmailList(body.emails); + + if (userIds.length === 0 && emails.length === 0) return []; + if (userIds.length + emails.length > MAX_TARGETED_RECIPIENTS) { + throw new Error(`单次指定发送最多 ${MAX_TARGETED_RECIPIENTS} 个收件人`); + } + + const result = await client.query( + `SELECT id, email + FROM profiles + WHERE COALESCE(role, 'user') NOT IN ('admin', 'enterprise_admin') + AND COALESCE(is_active, true) = true + AND COALESCE(email, '') <> '' + AND ( + id = ANY($1::uuid[]) + OR LOWER(email) = ANY($2::text[]) + )`, + [userIds, emails], + ); + + return result.rows + .map(row => ({ id: String(row.id), email: normalizeEmail(row.email) })) + .filter(row => isValidEmail(row.email)); +} + +export async function POST(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const client = await getDbClient(); + try { + const body = await request.json().catch(() => ({})) as Record; + const title = typeof body.title === 'string' ? body.title.trim().slice(0, 120) : ''; + const content = typeof body.content === 'string' ? body.content.trim().slice(0, 5000) : ''; + const buttonText = typeof body.buttonText === 'string' ? body.buttonText.trim().slice(0, 40) : ''; + const buttonUrl = typeof body.buttonUrl === 'string' ? body.buttonUrl.trim().slice(0, 500) : ''; + const mailKind = normalizeMailKind(body.mailKind); + const mailKindLabel = mailKind === 'admin' ? '管理员邮件' : '通知邮件'; + + if (!title || !content) { + return NextResponse.json({ error: '请填写邮件标题和正文内容' }, { status: 400 }); + } + if (buttonUrl && !/^https?:\/\/[^\s"'<>]+$/i.test(buttonUrl)) { + return NextResponse.json({ error: '按钮链接必须是 HTTP(S) 地址' }, { status: 400 }); + } + + const recipients = await loadRecipients(client, body); + const uniqueRecipients = [...new Map(recipients.map(item => [item.email, item])).values()]; + if (uniqueRecipients.length === 0) { + return NextResponse.json({ error: '没有可发送的非管理员用户邮箱' }, { status: 400 }); + } + + let sent = 0; + const failed: Array<{ email: string; error: string }> = []; + const assetBaseUrl = getRequestBaseUrl(request) || undefined; + + for (const recipient of uniqueRecipients) { + try { + await sendTemplatedEmail(client, { + to: recipient.email, + type: mailKind === 'admin' ? 'business' : 'announcement', + subject: `【妙境】${title}`, + title, + body: content, + buttonText: buttonText || undefined, + buttonUrl: buttonUrl || undefined, + note: `这是一封${mailKindLabel},请勿直接回复。`, + templateKind: mailKind, + ipAddress: body.mode === 'all' ? 'admin-broadcast' : 'admin-targeted', + assetBaseUrl, + }); + sent += 1; + } catch (error) { + failed.push({ + email: recipient.email, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return NextResponse.json({ + success: failed.length === 0, + total: uniqueRecipients.length, + sent, + failedCount: failed.length, + failed: failed.slice(0, 20), + message: failed.length === 0 + ? `邮件已发送给 ${sent} 个用户` + : `已发送 ${sent} 封,失败 ${failed.length} 封`, + }, { status: sent > 0 ? 200 : 400 }); + } catch (error) { + const message = error instanceof Error ? error.message : '邮件发送失败'; + return NextResponse.json({ error: message }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..55a2852 --- /dev/null +++ b/src/app/api/admin/stats/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const client = await getDbClient(); + try { + const result = await client.query(` + SELECT + COALESCE((SELECT total_visits FROM site_stats WHERE id = 1 LIMIT 1), 0)::int AS total_visits, + COALESCE(( + SELECT COUNT(*) + FROM profiles + WHERE COALESCE(role, 'user') NOT IN ('admin', 'enterprise_admin') + ), 0)::int AS total_users, + COALESCE(( + SELECT COUNT(*) + FROM works + WHERE is_public = true AND status = 'completed' + ), 0)::int AS total_works + `); + const row = result.rows[0] || {}; + return NextResponse.json({ + totalVisits: Number(row.total_visits || 0), + totalUsers: Number(row.total_users || 0), + totalWorks: Number(row.total_works || 0), + }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/stats] GET error:', err); + return NextResponse.json({ error: '获取统计数据失败' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/system-apis/route.ts b/src/app/api/admin/system-apis/route.ts new file mode 100644 index 0000000..77467e2 --- /dev/null +++ b/src/app/api/admin/system-apis/route.ts @@ -0,0 +1,142 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; +import { + encryptApiKeyForStorage, + ensureSystemApiSchema, + isUuid, + listSystemApis, + toSafeSystemApi, +} from '@/lib/server-api-config'; + +async function readBody(request: NextRequest) { + return request.json().catch(() => ({})); +} + +function normalizeType(value: unknown): 'image' | 'video' | 'text' { + return value === 'video' || value === 'text' ? value : 'image'; +} + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + const includeInactive = request.nextUrl.searchParams.get('includeInactive') !== 'false'; + return NextResponse.json({ apis: await listSystemApis(includeInactive) }); +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!body.name?.trim() || !body.modelName?.trim()) { + return NextResponse.json({ error: '请填写显示名称和模型名称' }, { status: 400 }); + } + + const secret = encryptApiKeyForStorage(String(body.apiKey || '')); + const client = await getDbClient(); + try { + await ensureSystemApiSchema(client); + const result = await client.query( + `INSERT INTO system_api_configs ( + provider, name, api_url, model_name, note, api_key_encrypted, + api_key_preview, type, credits_per_use, is_active, sort_order + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, + COALESCE((SELECT MAX(sort_order) + 1 FROM system_api_configs), 0)) + RETURNING id, provider, name, api_url, model_name, note, api_key_preview, + type, credits_per_use, is_active, sort_order, created_at, updated_at`, + [ + String(body.provider || '').trim(), + String(body.name).trim(), + String(body.apiUrl || '').trim(), + String(body.modelName).trim(), + String(body.note || '').trim(), + secret.encrypted, + secret.preview, + normalizeType(body.type), + Number(body.creditsPerUse || 10), + body.isActive !== false, + ], + ); + return NextResponse.json({ api: toSafeSystemApi(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + const body = await readBody(request); + if (!isUuid(body.id) || !body.name?.trim() || !body.modelName?.trim()) { + return NextResponse.json({ error: '缺少 API ID、显示名称或模型名称' }, { status: 400 }); + } + + const updates: string[] = []; + const params: unknown[] = []; + let idx = 1; + const add = (column: string, value: unknown) => { + updates.push(`${column} = $${idx++}`); + params.push(value); + }; + + add('provider', String(body.provider || '').trim()); + add('name', String(body.name).trim()); + add('api_url', String(body.apiUrl || '').trim()); + add('model_name', String(body.modelName).trim()); + add('note', String(body.note || '').trim()); + add('type', normalizeType(body.type)); + add('credits_per_use', Number(body.creditsPerUse || 10)); + add('is_active', body.isActive !== false); + if (body.sortOrder !== undefined) add('sort_order', Number(body.sortOrder || 0)); + + if (typeof body.apiKey === 'string' && body.apiKey.trim() && body.apiKey !== '********') { + const secret = encryptApiKeyForStorage(body.apiKey); + add('api_key_encrypted', secret.encrypted); + add('api_key_preview', secret.preview); + } + if (body.clearApiKey === true) { + add('api_key_encrypted', ''); + add('api_key_preview', ''); + } + updates.push('updated_at = NOW()'); + params.push(body.id); + + const client = await getDbClient(); + try { + await ensureSystemApiSchema(client); + const result = await client.query( + `UPDATE system_api_configs + SET ${updates.join(', ')} + WHERE id = $${idx} + RETURNING id, provider, name, api_url, model_name, note, api_key_preview, + type, credits_per_use, is_active, sort_order, created_at, updated_at`, + params, + ); + if (result.rows.length === 0) { + return NextResponse.json({ error: '系统 API 不存在' }, { status: 404 }); + } + return NextResponse.json({ api: toSafeSystemApi(result.rows[0]) }); + } finally { + client.release(); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + const body = await readBody(request); + const id = body.id || request.nextUrl.searchParams.get('id'); + if (!isUuid(id)) return NextResponse.json({ error: '缺少 API ID' }, { status: 400 }); + + const client = await getDbClient(); + try { + await ensureSystemApiSchema(client); + await client.query('DELETE FROM system_api_configs WHERE id = $1', [id]); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..253ca78 --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { requireAdmin } from '@/lib/admin-auth'; +import { deleteAdminUser, listAdminUsers, updateAdminUser } from '@/lib/admin-users-service'; + +function getTokenUserId(request: NextRequest): string | null { + const header = request.headers.get('authorization') || ''; + const token = header.replace(/^Bearer\s+/i, '').trim(); + const match = token.match(/^token-[a-z_]+-([0-9a-fA-F-]{36})-\d+$/); + return match?.[1] || null; +} + +export async function GET(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const client = await getDbClient(); + try { + const params = request.nextUrl.searchParams; + const result = await listAdminUsers(client, { + search: params.get('search') || params.get('q') || '', + page: Number(params.get('page') || '1'), + pageSize: Number(params.get('pageSize') || params.get('limit') || '20'), + }); + + return NextResponse.json(result); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/users] GET error:', err); + return NextResponse.json({ error: '获取用户列表失败' }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + const client = await getDbClient(); + try { + const result = await updateAdminUser(client, body); + return NextResponse.json(result.body, { status: result.status }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/users] PUT error:', err); + return NextResponse.json({ error: '服务器错误' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json().catch(() => ({})); + const userId = body.userId || body.id || request.nextUrl.searchParams.get('userId') || request.nextUrl.searchParams.get('id'); + const client = await getDbClient(); + try { + const result = await deleteAdminUser(client, String(userId || ''), getTokenUserId(request)); + return NextResponse.json(result.body, { status: result.status }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin/users] DELETE error:', err); + return NextResponse.json({ error: '删除用户失败' }, { status: 500 }); + } +} diff --git a/src/app/api/announcements/route.ts b/src/app/api/announcements/route.ts new file mode 100644 index 0000000..4c9aaee --- /dev/null +++ b/src/app/api/announcements/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { requireAdmin } from '@/lib/admin-auth'; + +function toPublicAnnouncement(row: Record) { + const startsAt = row.starts_at ?? row.start_date ?? null; + const expiresAt = row.expires_at ?? row.end_date ?? null; + const isActive = row.is_active ?? row.enabled ?? true; + + return { + ...row, + enabled: isActive !== false, + start_date: startsAt, + end_date: expiresAt, + is_active: isActive !== false, + starts_at: startsAt, + expires_at: expiresAt, + }; +} + +export async function GET() { + try { + const client = await getDbClient(); + try { + const result = await client.query('SELECT * FROM announcements ORDER BY created_at DESC'); + return NextResponse.json((result.rows || []).map(toPublicAnnouncement)); + } finally { + client.release(); + } + } catch { + return NextResponse.json([]); + } +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + const { title, content, startDate, endDate, enabled } = body; + + if (!title || !content || !startDate || !endDate) { + return NextResponse.json({ error: '请填写完整公告信息' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const id = crypto.randomUUID(); + await client.query( + 'INSERT INTO announcements (id, title, content, is_active, starts_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6)', + [id, title, content, enabled !== false, new Date(startDate).toISOString(), new Date(endDate).toISOString()] + ); + return NextResponse.json({ id, success: true }); + } finally { + client.release(); + } + } catch (err) { + console.error('[announcements] POST error:', err); + return NextResponse.json({ error: '创建公告失败' }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + const { id, title, content, startDate, endDate, enabled } = body; + + if (!id) { + return NextResponse.json({ error: '缺少公告ID' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const updates: string[] = []; + const params: unknown[] = []; + let paramIdx = 1; + + if (title !== undefined) { updates.push(`title = $${paramIdx++}`); params.push(title); } + if (content !== undefined) { updates.push(`content = $${paramIdx++}`); params.push(content); } + if (startDate !== undefined) { updates.push(`starts_at = $${paramIdx++}`); params.push(new Date(startDate).toISOString()); } + if (endDate !== undefined) { updates.push(`expires_at = $${paramIdx++}`); params.push(new Date(endDate).toISOString()); } + if (enabled !== undefined) { updates.push(`is_active = $${paramIdx++}`); params.push(enabled); } + updates.push(`updated_at = NOW()`); + + params.push(id); + await client.query(`UPDATE announcements SET ${updates.join(', ')} WHERE id = $${paramIdx}`, params); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } + } catch (err) { + console.error('[announcements] PUT error:', err); + return NextResponse.json({ error: '更新公告失败' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: '缺少公告ID' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + await client.query('DELETE FROM announcements WHERE id = $1', [id]); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } + } catch (err) { + console.error('[announcements] DELETE error:', err); + return NextResponse.json({ error: '删除公告失败' }, { status: 500 }); + } +} diff --git a/src/app/api/auth/admin-exists/route.ts b/src/app/api/auth/admin-exists/route.ts new file mode 100644 index 0000000..9d202e7 --- /dev/null +++ b/src/app/api/auth/admin-exists/route.ts @@ -0,0 +1,78 @@ +import { NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { ensureEmailSchema } from '@/lib/email-service'; +import { getRequiredProductionSecret, isProductionRuntime } from '@/lib/runtime-env'; +import { ensureProfilePreferenceSchema } from '@/lib/profile-preferences'; + +const ADMIN_EMAIL = 'admin@miaojing.ai'; + +export async function GET() { + try { + const client = await getDbClient(); + + try { + await ensureEmailSchema(client); + await ensureProfilePreferenceSchema(client); + const result = await client.query( + 'SELECT id, nickname FROM profiles WHERE role = $1 LIMIT 1', + ['admin'] + ); + + if (result.rows.length > 0) { + return NextResponse.json({ exists: true, nickname: result.rows[0].nickname }); + } + + if (isProductionRuntime()) { + return NextResponse.json({ exists: false, autoCreated: false }); + } + + getRequiredProductionSecret('ADMIN_DEFAULT_PASSWORD', 'admin123'); + + // Development only: bootstrap the default admin profile. + const userId = crypto.randomUUID(); + + await client.query( + 'INSERT INTO auth.users (id, email, created_at) VALUES ($1, $2, NOW())', + [userId, ADMIN_EMAIL] + ); + + await client.query( + `INSERT INTO profiles ( + id, email, nickname, role, membership_tier, credits_balance, + daily_quota_limit, daily_quota_used, is_active, email_verified, + email_verified_at, email_bound_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, true, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET + role = $4, + membership_tier = $5, + credits_balance = $6, + daily_quota_limit = $7, + nickname = $3, + email_verified = true, + email_verified_at = COALESCE(profiles.email_verified_at, NOW()), + email_bound_at = COALESCE(profiles.email_bound_at, NOW())`, + [userId, ADMIN_EMAIL, '管理员', 'admin', 'enterprise', 9999, 999, 0, true] + ); + + try { + await client.query( + 'INSERT INTO credit_transactions (user_id, amount, balance_after, type, description) VALUES ($1, $2, $3, $4, $5)', + [userId, 9999, 9999, 'gift', '管理员初始积分'] + ); + } catch { /* non-critical */ } + + console.log('[admin-exists] Default admin account created: account=admin, password=***'); + return NextResponse.json({ + exists: true, + autoCreated: true, + nickname: '管理员', + }); + } finally { + client.release(); + } + } catch (err) { + console.error('[admin-exists] Error:', err); + return NextResponse.json({ exists: false, error: '数据库连接失败' }); + } +} diff --git a/src/app/api/auth/fetch-models/route.ts b/src/app/api/auth/fetch-models/route.ts new file mode 100644 index 0000000..842dc4a --- /dev/null +++ b/src/app/api/auth/fetch-models/route.ts @@ -0,0 +1,146 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError } from '@/lib/custom-api-fetch'; + +interface FetchModelsRequest { + apiUrl: string; + apiKey: string; + provider: string; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { apiUrl, apiKey, provider } = body as FetchModelsRequest; + + if (!apiUrl || !apiKey) { + return NextResponse.json( + { success: false, error: '请填写 API 请求地址和 API Key' }, + { status: 400 } + ); + } + + // Derive the base URL from the apiUrl + const baseUrl = apiUrl.replace(/\/images\/generations.*/, '').replace(/\/videos\/generations.*/, '').replace(/\/chat\/completions.*/, '').replace(/\/+$/, ''); + const modelsUrl = `${baseUrl}/models`; + + let response: Response; + try { + response = await fetchWithRetry( + modelsUrl, + { + method: 'GET', + headers: buildCustomApiHeaders(apiKey), + }, + 15_000, + 0, // no retry + ); + } catch (fetchError: unknown) { + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + return NextResponse.json({ + success: false, + error: `网络错误: ${msg}`, + suggestion: '请检查 API 地址是否正确、网络是否可达', + }); + } + + if (response.ok) { + try { + const data = await response.json(); + if (Array.isArray(data.data)) { + const models = data.data.map((m: Record) => ({ + id: typeof m.id === 'string' ? m.id : '', + name: typeof m.name === 'string' ? m.name : '', + description: typeof m.description === 'string' ? m.description : '', + provider: provider, + })).filter((m: { id: string }) => m.id); + + return NextResponse.json({ + success: true, + models: models, + message: `成功获取 ${models.length} 个模型`, + }); + } else { + return NextResponse.json({ + success: false, + error: 'API 返回的数据格式不正确', + suggestion: '请检查 API 地址是否正确,确保它支持 /models 端点', + }); + } + } catch (parseError) { + return NextResponse.json({ + success: false, + error: '解析模型数据失败', + suggestion: 'API 返回的数据格式可能不正确', + }); + } + } else { + const errorText = await response.text().catch(() => ''); + const isHtml = errorText.trim().startsWith(' = { + 401: 'API Key 无效或已过期,请检查密钥是否正确', + 403: '账户无权限访问该模型,请检查账户状态', + 404: 'API 地址不正确,请确认完整的请求端点 URL', + 429: '请求频率过高或账户余额不足', + 500: 'API 服务端内部错误,请稍后重试', + 502: 'API 网关错误。可能原因:①API 服务端宕机 ②代理防火墙拦截了服务器 IP', + 503: '服务暂不可用。可能原因:①账户余额不足 ②服务维护中 ③代理限制了服务器IP', + }; + + return suggestions[statusCode] || ''; +} + +/** + * Parse common API error status codes and bodies into user-friendly messages + */ +function parseApiError(statusCode: number, errorBody: string): { error: string; suggestion: string } { + // Delegate HTML detection to shared utility + const friendlyError = parseCustomApiError(statusCode, errorBody); + + const suggestions: Record = { + 401: 'API Key 无效或已过期,请检查密钥是否正确', + 403: '账户无权限访问该模型,请检查账户状态', + 404: 'API 地址不正确,请确认完整的请求端点 URL', + 429: '请求频率过高或账户余额不足', + 500: 'API 服务端内部错误,请稍后重试', + 502: 'API 网关错误。可能原因:①API 服务端宕机 ②代理防火墙拦截了服务器 IP', + 503: '服务暂不可用。可能原因:①账户余额不足 ②服务维护中 ③代理限制了服务器IP', + }; + + return { + error: friendlyError, + suggestion: suggestions[statusCode] || '', + }; +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..905b9df --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,290 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { ensureEmailSchema } from '@/lib/email-service'; +import { createSessionToken } from '@/lib/session-auth'; +import { getRequiredProductionSecret } from '@/lib/runtime-env'; +import { writePlatformLog } from '@/lib/platform-logs'; +import { ensureProfilePreferenceSchema, normalizePreferredTheme } from '@/lib/profile-preferences'; + +function normalizeRoleForTier(role: string | null | undefined, tier: string | null | undefined): string { + const currentRole = role || 'user'; + if (currentRole === 'admin' || currentRole === 'enterprise_admin') return currentRole; + return tier && tier !== 'free' ? 'vip' : currentRole === 'vip' ? 'user' : currentRole; +} + +async function verifyPasswordHash(client: Awaited>, passwordHash: string, password: string): Promise { + const result = await client.query( + 'SELECT $1::text = crypt($2::text, $1::text) AS ok', + [passwordHash, password] + ); + return result.rows[0]?.ok === true; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email: rawEmail, account, phone: rawPhone, password, adminOnly } = body; + + const identifier = account || rawEmail || rawPhone; + if (!identifier || !password) { + return NextResponse.json({ error: 'Please enter account and password' }, { status: 400 }); + } + + const client = await getDbClient(); + + try { + await ensureEmailSchema(client); + await ensureProfilePreferenceSchema(client); + let loginEmail = identifier; + let userId = ''; + let userRole = 'user'; + let userNickname = ''; + let userMembershipTier = 'free'; + let userCreditsBalance = 0; + let userDailyQuotaUsed = 0; + let userDailyQuotaLimit = 5; + let userAvatarUrl: string | null = null; + let userPhone: string | null = null; + let userCreatedAt: string | null = null; + let userEmailVerified = false; + let userEmailVerifiedAt: string | null = null; + let userPreferredTheme: 'dark' | 'light' = 'dark'; + + const isEmailFormat = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier); + let isAdminAccount = false; + let adminProfileId: string | null = null; + + if (!isEmailFormat) { + const adminLookup = await client.query( + "SELECT id, email, nickname, role FROM profiles WHERE (nickname = $1 OR phone = $1) AND role = 'admin' LIMIT 1", + [identifier] + ); + if (adminLookup.rows.length > 0) { + isAdminAccount = true; + adminProfileId = adminLookup.rows[0].id; + loginEmail = adminLookup.rows[0].email; + userNickname = adminLookup.rows[0].nickname || ''; + } else { + const nicknameLower = String(identifier).toLowerCase(); + if (nicknameLower === 'admin' || nicknameLower.startsWith('admin')) { + const anyLookup = await client.query( + "SELECT id, email, nickname, role FROM profiles WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1" + ); + if (anyLookup.rows.length > 0) { + isAdminAccount = true; + adminProfileId = anyLookup.rows[0].id; + loginEmail = anyLookup.rows[0].email; + userNickname = anyLookup.rows[0].nickname || ''; + } + } + } + } else { + const adminLookup = await client.query( + "SELECT id, email, nickname, role FROM profiles WHERE email = $1 AND role = 'admin' LIMIT 1", + [identifier] + ); + if (adminLookup.rows.length > 0) { + isAdminAccount = true; + adminProfileId = adminLookup.rows[0].id; + loginEmail = identifier; + userNickname = adminLookup.rows[0].nickname || ''; + } + } + + if (isAdminAccount) { + const authResult = await client.query( + 'SELECT id, email, created_at, password_hash FROM auth.users WHERE email = $1', + [loginEmail] + ); + + if (authResult.rows.length > 0 && authResult.rows[0].password_hash) { + const passwordOk = await verifyPasswordHash(client, authResult.rows[0].password_hash, password); + if (!passwordOk) { + return NextResponse.json({ error: 'Invalid admin password' }, { status: 401 }); + } + } else if (password !== getRequiredProductionSecret('ADMIN_DEFAULT_PASSWORD', 'admin123')) { + return NextResponse.json({ error: 'Invalid admin password' }, { status: 401 }); + } + + userRole = 'admin'; + userMembershipTier = 'enterprise'; + userCreditsBalance = 9999; + userDailyQuotaLimit = 999; + userNickname = userNickname || '管理员'; + userEmailVerified = true; + userEmailVerifiedAt = new Date().toISOString(); + + if (authResult.rows.length > 0) { + userId = authResult.rows[0].id; + userCreatedAt = authResult.rows[0].created_at; + } else if (adminProfileId) { + userId = adminProfileId; + await client.query( + 'INSERT INTO auth.users (id, email, created_at) VALUES ($1, $2, NOW()) ON CONFLICT (id) DO NOTHING', + [userId, loginEmail] + ); + userCreatedAt = new Date().toISOString(); + } else { + userId = crypto.randomUUID(); + await client.query( + 'INSERT INTO auth.users (id, email, created_at) VALUES ($1, $2, NOW())', + [userId, loginEmail] + ); + userCreatedAt = new Date().toISOString(); + } + + await client.query( + `INSERT INTO profiles (id, email, nickname, role, membership_tier, credits_balance, daily_quota_limit, daily_quota_used, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (id) DO UPDATE SET + role = $4, + membership_tier = $5, + credits_balance = $6, + daily_quota_limit = $7, + nickname = $3, + is_active = true, + email_verified = true, + email_verified_at = COALESCE(profiles.email_verified_at, NOW()), + email_bound_at = COALESCE(profiles.email_bound_at, NOW())`, + [userId, loginEmail, userNickname, 'admin', 'enterprise', 9999, 999, 0, true] + ); + + const adminThemeResult = await client.query( + 'SELECT preferred_theme FROM profiles WHERE id = $1 LIMIT 1', + [userId] + ); + userPreferredTheme = normalizePreferredTheme(adminThemeResult.rows[0]?.preferred_theme); + + if (adminProfileId && adminProfileId !== userId) { + await client.query( + 'UPDATE profiles SET role = $1, membership_tier = $2, credits_balance = $3, daily_quota_limit = $4 WHERE id = $5', + ['admin', 'enterprise', 9999, 999, adminProfileId] + ); + } + } else { + if (!isEmailFormat) { + const profileResult = await client.query( + 'SELECT id, email, nickname, phone, role FROM profiles WHERE nickname = $1 OR phone = $1 LIMIT 1', + [identifier] + ); + + if (profileResult.rows.length > 0) { + const profile = profileResult.rows[0]; + loginEmail = profile.email; + userId = profile.id; + userRole = profile.role || 'user'; + userNickname = profile.nickname; + userPhone = profile.phone; + } else { + return NextResponse.json({ error: 'Account does not exist' }, { status: 401 }); + } + } + + const authResult = await client.query( + 'SELECT id, email, created_at, password_hash FROM auth.users WHERE email = $1', + [loginEmail] + ); + + if (authResult.rows.length === 0) { + return NextResponse.json({ error: 'Account does not exist' }, { status: 401 }); + } + + const authUser = authResult.rows[0]; + if (authUser.password_hash) { + const passwordOk = await verifyPasswordHash(client, authUser.password_hash, password); + if (!passwordOk) { + return NextResponse.json({ error: 'Invalid password' }, { status: 401 }); + } + } else { + return NextResponse.json({ error: '该账号缺少密码凭据,请联系管理员重置密码后再登录' }, { status: 401 }); + } + + userId = authUser.id; + userCreatedAt = authUser.created_at; + + const profileResult = await client.query( + 'SELECT nickname, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit, avatar_url, phone, email_verified, email_verified_at, preferred_theme FROM profiles WHERE id = $1', + [userId] + ); + + if (profileResult.rows.length > 0) { + const profile = profileResult.rows[0]; + userNickname = profile.nickname || loginEmail.split('@')[0]; + userMembershipTier = profile.membership_tier || 'free'; + userRole = normalizeRoleForTier(profile.role, userMembershipTier); + userCreditsBalance = profile.credits_balance || 0; + userDailyQuotaUsed = profile.daily_quota_used || 0; + userDailyQuotaLimit = profile.daily_quota_limit || 5; + userAvatarUrl = profile.avatar_url || null; + userPhone = profile.phone || null; + userEmailVerified = profile.email_verified === true; + userEmailVerifiedAt = profile.email_verified_at || null; + userPreferredTheme = normalizePreferredTheme(profile.preferred_theme); + if (userRole !== (profile.role || 'user')) { + await client.query('UPDATE profiles SET role = $1, updated_at = NOW() WHERE id = $2', [userRole, userId]); + } + } else { + userNickname = loginEmail.split('@')[0]; + await client.query( + `INSERT INTO profiles (id, email, nickname, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (id) DO UPDATE SET email = $2, nickname = $3, email_verified = false, email_verified_at = NULL`, + [userId, loginEmail, userNickname, userRole, userMembershipTier, userCreditsBalance, userDailyQuotaUsed, userDailyQuotaLimit] + ); + } + } + + if (adminOnly === true && userRole !== 'admin' && userRole !== 'enterprise_admin') { + void writePlatformLog({ + type: 'security', + level: 'warning', + action: 'console_login_denied', + message: '非管理员账号尝试登录管理后台被拒绝', + userId, + userName: userNickname, + userEmail: loginEmail, + request, + }); + return NextResponse.json({ error: 'Only administrators can log in to the console' }, { status: 403 }); + } + + const accessToken = createSessionToken(userId, userRole); + void writePlatformLog({ + type: 'auth', + level: 'info', + action: adminOnly === true ? 'console_login_success' : 'user_login_success', + message: adminOnly === true ? '管理员登录管理后台成功' : '用户登录成功', + userId, + userName: userNickname, + userEmail: loginEmail, + request, + }); + + return NextResponse.json({ + user: { + id: userId, + email: loginEmail, + nickname: userNickname, + role: userRole, + membership_tier: userMembershipTier, + credits_balance: userCreditsBalance, + daily_quota_used: userDailyQuotaUsed, + daily_quota_limit: userDailyQuotaLimit, + avatar_url: userAvatarUrl, + phone: userPhone, + created_at: userCreatedAt, + email_verified: userEmailVerified, + email_verified_at: userEmailVerifiedAt, + preferred_theme: userPreferredTheme, + }, + session: { access_token: accessToken }, + }); + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Login failed'; + console.error('[Login Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..bebfe21 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,175 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { ensureEmailSchema, getRequestBaseUrl, normalizeEmail, sendTemplatedEmail, verifyEmailCode } from '@/lib/email-service'; +import { getRequiredProductionSecret } from '@/lib/runtime-env'; +import { ensureProfilePreferenceSchema } from '@/lib/profile-preferences'; + +function isStrongPassword(password: string): boolean { + return password.length >= 8 && /[A-Za-z]/.test(password) && /\d/.test(password); +} + +export async function POST(request: NextRequest) { + try { + const { email, password, nickname, phone, inviteCode, emailCode, acceptedTerms } = await request.json(); + const normalizedEmail = normalizeEmail(email); + + if (!normalizedEmail || !password) { + return NextResponse.json({ error: 'Please enter email and password' }, { status: 400 }); + } + + if (acceptedTerms !== true) { + return NextResponse.json({ error: '请先阅读并同意服务条款和隐私政策' }, { status: 400 }); + } + + if (!isStrongPassword(password)) { + return NextResponse.json({ error: '密码至少 8 位,并同时包含字母和数字' }, { status: 400 }); + } + + const isAdminRegistration = typeof inviteCode === 'string' + && inviteCode === getRequiredProductionSecret('ADMIN_INVITE_CODE', 'miaojing-admin-2024'); + const client = await getDbClient(); + + try { + await ensureEmailSchema(client); + await ensureProfilePreferenceSchema(client); + if (isAdminRegistration) { + const existingAdminResult = await client.query( + 'SELECT id FROM profiles WHERE role = $1', + ['admin'] + ); + + if (existingAdminResult.rows.length > 0) { + return NextResponse.json( + { error: 'Admin account already exists' }, + { status: 400 } + ); + } + } + + const existingUserResult = await client.query( + 'SELECT id FROM profiles WHERE email = $1', + [normalizedEmail] + ); + + if (existingUserResult.rows.length > 0) { + return NextResponse.json( + { error: 'Email is already registered' }, + { status: 400 } + ); + } + + const userId = crypto.randomUUID(); + + if (!isAdminRegistration) { + if (typeof emailCode !== 'string' || !/^[a-z0-9]{4,10}$/i.test(emailCode)) { + return NextResponse.json({ error: '请输入正确的邮箱验证码' }, { status: 400 }); + } + await client.query('BEGIN'); + try { + await verifyEmailCode(client, { + email: normalizedEmail, + type: 'register', + code: typeof emailCode === 'string' ? emailCode : '', + }); + await client.query('COMMIT'); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } + } + + await client.query( + `INSERT INTO auth.users (id, email, password_hash, created_at) + VALUES ($1, $2, crypt($3, gen_salt('bf')), NOW())`, + [userId, normalizedEmail, password] + ); + + const role = isAdminRegistration ? 'admin' : 'user'; + const membershipTier = isAdminRegistration ? 'enterprise' : 'free'; + const creditsBalance = isAdminRegistration ? 9999 : 10; + const dailyQuotaLimit = isAdminRegistration ? 999 : 5; + const displayName = nickname || normalizedEmail.split('@')[0]; + + await client.query( + `INSERT INTO profiles ( + id, email, nickname, phone, role, membership_tier, credits_balance, + daily_quota_limit, daily_quota_used, is_active, email_verified, + email_verified_at, email_bound_at, email_sender_domain + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CASE WHEN $11 THEN NOW() ELSE NULL END, CASE WHEN $11 THEN NOW() ELSE NULL END, $12) + ON CONFLICT (id) DO UPDATE SET + email = EXCLUDED.email, + nickname = EXCLUDED.nickname, + phone = EXCLUDED.phone, + role = EXCLUDED.role, + membership_tier = EXCLUDED.membership_tier, + credits_balance = EXCLUDED.credits_balance, + daily_quota_limit = EXCLUDED.daily_quota_limit, + daily_quota_used = EXCLUDED.daily_quota_used, + is_active = EXCLUDED.is_active, + email_verified = EXCLUDED.email_verified, + email_verified_at = EXCLUDED.email_verified_at, + email_bound_at = EXCLUDED.email_bound_at, + email_sender_domain = EXCLUDED.email_sender_domain`, + [ + userId, + normalizedEmail, + displayName, + phone || null, + role, + membershipTier, + creditsBalance, + dailyQuotaLimit, + 0, + true, + true, + normalizedEmail.split('@')[1] || null, + ] + ); + + try { + await client.query( + 'INSERT INTO credit_transactions (user_id, amount, balance_after, type, description) VALUES ($1, $2, $3, $4, $5)', + [userId, creditsBalance, creditsBalance, 'gift', isAdminRegistration ? 'Admin initial credits' : 'New user registration bonus'] + ); + } catch { + // Ignore credit transaction errors. + } + + await sendTemplatedEmail(client, { + to: normalizedEmail, + type: 'register_success', + subject: '【妙境】注册成功', + title: '注册成功', + intro: isAdminRegistration ? '管理员账号已创建成功。' : '你的妙境账号已注册成功,邮箱也已完成验证。', + note: '若非本人操作,请尽快联系管理员。', + assetBaseUrl: getRequestBaseUrl(request) || undefined, + }).catch(() => undefined); + + return NextResponse.json({ + user: { + id: userId, + email: normalizedEmail, + nickname: displayName, + role, + membership_tier: membershipTier, + credits_balance: creditsBalance, + daily_quota_used: 0, + daily_quota_limit: dailyQuotaLimit, + avatar_url: null, + phone: phone || null, + email_verified: true, + email_verified_at: new Date().toISOString(), + preferred_theme: 'dark', + }, + message: isAdminRegistration ? 'Admin account registered' : 'Registration successful', + }); + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Registration failed'; + console.error('[Register Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/auth/test-api/route.ts b/src/app/api/auth/test-api/route.ts new file mode 100644 index 0000000..da9f17a --- /dev/null +++ b/src/app/api/auth/test-api/route.ts @@ -0,0 +1,215 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError } from '@/lib/custom-api-fetch'; + +interface TestApiRequest { + apiUrl: string; + apiKey: string; + modelName: string; + provider: string; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { apiUrl, apiKey, modelName, provider } = body as TestApiRequest; + + if (!apiUrl || !apiKey) { + return NextResponse.json( + { success: false, error: '请填写 API 请求地址和 API Key' }, + { status: 400 } + ); + } + + // ---- Step 1: Quick connectivity check with a lightweight request ---- + // Try the /models endpoint first (most APIs support this, no cost) + // Derive the base URL from the apiUrl + const baseUrl = apiUrl.replace(/\/images\/generations.*/, '').replace(/\/videos\/generations.*/, '').replace(/\/chat\/completions.*/, '').replace(/\/+$/, ''); + const modelsUrl = `${baseUrl}/models`; + + let response: Response; + try { + response = await fetchWithRetry( + modelsUrl, + { + method: 'GET', + headers: buildCustomApiHeaders(apiKey), + }, + 15_000, + 0, // no retry for test - keep it fast + ); + } catch (fetchError: unknown) { + // If /models fails with timeout or network error, try the actual endpoint + if (fetchError instanceof DOMException && fetchError.name === 'AbortError') { + return await testActualEndpoint(apiUrl, apiKey, modelName || 'gpt-image-2'); + } + + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + + // Network error - could be DNS, connection refused, or firewall + if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('fetch failed')) { + return NextResponse.json({ + success: false, + error: `无法连接到 API 地址: ${msg}`, + suggestion: '请检查 API 地址是否正确、服务是否运行。常见原因:①地址拼写错误 ②服务未启动 ③DNS 无法解析', + }); + } + + return NextResponse.json({ + success: false, + error: `网络错误: ${msg}`, + suggestion: '请检查 API 地址是否正确、网络是否可达。如果使用了代理(如 Cloudflare),可能代理防火墙拦截了服务器请求', + }); + } + + // If /models returned successfully, the key is valid + if (response.ok) { + let modelInfo = ''; + try { + const data = await response.json(); + if (Array.isArray(data.data)) { + const targetModel = modelName || 'gpt-image-2'; + const found = data.data.some((m: Record) => + typeof m.id === 'string' && m.id.includes(targetModel.replace('gpt-image-2', 'dall')) + ); + modelInfo = found ? `,模型 ${modelName || 'gpt-image-2'} 可用` : `,已连接(共 ${data.data.length} 个模型)`; + } + } catch { + // Ignore parse error, connectivity is confirmed + } + return NextResponse.json({ + success: true, + message: `连接成功${modelInfo}`, + }); + } + + // /models returned an error - check if it's HTML (Cloudflare block) + const errorText = await response.text().catch(() => ''); + const isHtml = errorText.trim().startsWith(' { + try { + const response = await fetchWithRetry( + apiUrl, + { + method: 'POST', + headers: buildCustomApiHeaders(apiKey), + body: JSON.stringify({ + model: modelName, + prompt: 'test', + n: 1, + size: '1024x1024', + }), + }, + 15_000, + 0, // no retry for test + ); + + if (response.ok) { + return NextResponse.json({ + success: true, + message: `连接成功,模型 ${modelName} 可用`, + }); + } + + const errorText = await response.text().catch(() => ''); + const parsed = parseApiError(response.status, errorText); + + return NextResponse.json({ + success: false, + error: parsed.error, + statusCode: response.status, + suggestion: parsed.suggestion, + }); + } catch (fetchError: unknown) { + if (fetchError instanceof DOMException && fetchError.name === 'AbortError') { + return NextResponse.json({ + success: false, + error: '连接超时(15秒),请检查 API 地址是否正确', + suggestion: '可能原因:①API 地址有误 ②服务响应过慢 ③代理限制了服务器IP访问', + }); + } + + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + return NextResponse.json({ + success: false, + error: `网络错误: ${msg}`, + suggestion: '请检查 API 地址和网络连通性', + }); + } +} + +/** + * Get diagnostic suggestion based on response status and content type + */ +function getDiagnosticSuggestion(statusCode: number, isHtml: boolean): string { + if (isHtml) { + if (statusCode === 502 || statusCode === 503 || statusCode === 504) { + return 'API 代理(如 Cloudflare)返回错误。你的 API 在本地可用但部署环境不可用时,通常是代理防火墙拦截了服务器请求。建议:①检查 API 代理的 WAF/防火墙设置 ②将服务器 IP 加入白名单 ③尝试使用 API 的直连地址(绕过 Cloudflare)'; + } + if (statusCode === 403) { + return '代理防火墙拦截了请求。建议:①检查 Cloudflare WAF 规则 ②将服务器 IP 加入白名单 ③使用 API 的直连地址'; + } + return 'API 返回了错误页面而非 JSON 响应,可能是代理防火墙拦截。建议使用 API 的直连地址(绕过 CDN/代理)'; + } + + const suggestions: Record = { + 401: 'API Key 无效或已过期,请检查密钥是否正确', + 403: '账户无权限访问该模型,请检查账户状态', + 404: 'API 地址不正确,请确认完整的请求端点 URL', + 429: '请求频率过高或账户余额不足', + 500: 'API 服务端内部错误,请稍后重试', + 502: 'API 网关错误。可能原因:①API 服务端宕机 ②代理防火墙拦截了服务器 IP', + 503: '服务暂不可用。可能原因:①账户余额不足 ②服务维护中 ③代理限制了服务器IP', + }; + + return suggestions[statusCode] || ''; +} + +/** + * Parse common API error status codes and bodies into user-friendly messages + */ +function parseApiError(statusCode: number, errorBody: string): { error: string; suggestion: string } { + // Delegate HTML detection to shared utility + const friendlyError = parseCustomApiError(statusCode, errorBody); + + const suggestions: Record = { + 401: 'API Key 无效或已过期,请检查密钥是否正确', + 403: '账户无权限访问该模型,请检查账户状态', + 404: 'API 地址不正确,请确认完整的请求端点 URL', + 429: '请求频率过高或账户余额不足', + 500: 'API 服务端内部错误,请稍后重试', + 502: 'API 网关错误。可能原因:①API 服务端宕机 ②代理防火墙拦截了服务器 IP', + 503: '服务暂不可用。可能原因:①账户余额不足 ②服务维护中 ③代理限制了服务器IP', + }; + + return { + error: friendlyError, + suggestion: suggestions[statusCode] || '', + }; +} diff --git a/src/app/api/creation-history/route.ts b/src/app/api/creation-history/route.ts new file mode 100644 index 0000000..ccf138e --- /dev/null +++ b/src/app/api/creation-history/route.ts @@ -0,0 +1,143 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; + +function toWorkType(type: string, params: Record): string { + const explicitMode = params.creationMode || params.workType || params.mode; + if (explicitMode === 'text2img' || explicitMode === 'img2img' || explicitMode === 'text2video' || explicitMode === 'img2video' || explicitMode === 'reverse-prompt') { + return explicitMode; + } + if (type === 'reverse-prompt') return 'reverse-prompt'; + const hasReference = Boolean(params.referenceImage) + || (Array.isArray(params.referenceImages) && params.referenceImages.length > 0) + || Number(params.refImageCount || 0) > 0; + if (type === 'video') return hasReference ? 'img2video' : 'text2video'; + return hasReference ? 'img2img' : 'text2img'; +} + +function fromWorkType(type: string): 'image' | 'video' | 'reverse-prompt' { + if (type === 'reverse-prompt') return 'reverse-prompt'; + return type.includes('video') ? 'video' : 'image'; +} + +function mapWork(row: Record) { + const params = (row.params || {}) as Record; + return { + id: row.id, + type: fromWorkType(String(row.type || 'text2img')), + url: row.result_url, + prompt: row.prompt || '', + negativePrompt: row.negative_prompt || undefined, + model: params.model || '', + modelLabel: params.modelLabel || params.model || '', + isCustomModel: Boolean(params.isCustomModel), + params, + referenceImage: params.referenceImage, + referenceImages: Array.isArray(params.referenceImages) + ? params.referenceImages + : params.referenceImage + ? [params.referenceImage] + : undefined, + published: row.is_public === true, + createdAt: row.created_at, + }; +} + +export async function GET(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + const client = await getDbClient(); + try { + const result = await client.query( + `SELECT id, type, prompt, negative_prompt, params, result_url, is_public, status, created_at + FROM works + WHERE user_id = $1 AND status = 'completed' + ORDER BY created_at DESC + LIMIT 300`, + [userId], + ); + return NextResponse.json({ records: result.rows.map(mapWork) }); + } finally { + client.release(); + } +} + +export async function POST(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + const body = await request.json(); + const records = Array.isArray(body.records) ? body.records : [body]; + const client = await getDbClient(); + try { + await client.query('BEGIN'); + const saved = []; + for (const record of records) { + const params = { + ...(record.params || {}), + model: record.model || (record.params || {}).model, + modelLabel: record.modelLabel || (record.params || {}).modelLabel, + isCustomModel: Boolean(record.isCustomModel), + referenceImage: record.referenceImage || (record.params || {}).referenceImage, + referenceImages: record.referenceImages || (record.params || {}).referenceImages, + }; + const workType = toWorkType(String(record.type || 'image'), params); + let url = String(record.url || '').trim(); + if (workType === 'reverse-prompt') { + url = url && !url.startsWith('data:') ? url : `[reverse-prompt:${record.id || Date.now()}]`; + } + if (!url || url.startsWith('data:')) continue; + const existing = await client.query( + `SELECT id, type, prompt, negative_prompt, params, result_url, is_public, status, created_at + FROM works + WHERE user_id = $1 AND result_url = $2 + LIMIT 1`, + [userId, url], + ); + if (existing.rows[0]) { + saved.push(mapWork(existing.rows[0])); + continue; + } + const result = await client.query( + `INSERT INTO works (user_id, type, prompt, negative_prompt, params, result_url, is_public, status, credits_cost, created_at) + VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, 'completed', $8, COALESCE($9::timestamptz, NOW())) + RETURNING id, type, prompt, negative_prompt, params, result_url, is_public, status, created_at`, + [ + userId, + workType, + record.prompt || '', + record.negativePrompt || null, + JSON.stringify(params), + url, + Boolean(record.published), + Number(record.creditsCost || 0), + record.createdAt || null, + ], + ); + if (result.rows[0]) saved.push(mapWork(result.rows[0])); + } + await client.query('COMMIT'); + return NextResponse.json({ records: saved }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +export async function DELETE(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + const id = request.nextUrl.searchParams.get('id'); + const client = await getDbClient(); + try { + if (id) { + await client.query('DELETE FROM works WHERE id = $1 AND user_id = $2', [id, userId]); + } else { + await client.query('DELETE FROM works WHERE user_id = $1', [userId]); + } + return NextResponse.json({ success: true }); + } finally { + client.release(); + } +} diff --git a/src/app/api/download/route.ts b/src/app/api/download/route.ts new file mode 100644 index 0000000..3e2106b --- /dev/null +++ b/src/app/api/download/route.ts @@ -0,0 +1,158 @@ +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; +import { localStorage } from '@/lib/local-storage'; +import { fetchPublicHttpUrl } from '@/lib/remote-fetch'; + +/** + * Download proxy. + * + * Supports: + * - remote http(s) URLs, fetched server-side to avoid browser CORS failures + * - same-origin relative URLs + * - local-storage URLs, read directly from disk with path traversal protection + */ +export async function GET(request: NextRequest) { + const url = request.nextUrl.searchParams.get('url'); + const filename = sanitizeFilename( + request.nextUrl.searchParams.get('filename') || 'download', + ); + + if (!url) { + return NextResponse.json({ error: '缺少 url 参数' }, { status: 400 }); + } + + try { + const localKey = getLocalStorageKey(url); + if (localKey) { + return downloadLocalStorageFile(localKey, filename); + } + + const targetUrl = resolveDownloadUrl(url, request.nextUrl.origin); + if (!targetUrl) { + return NextResponse.json( + { error: '仅支持 HTTP(S) URL 或站内文件 URL' }, + { status: 400 }, + ); + } + + const response = await fetchPublicHttpUrl(targetUrl, { + signal: AbortSignal.timeout(60_000), + }); + + if (!response.ok) { + return NextResponse.json( + { error: `远程文件获取失败: ${response.status}` }, + { status: response.status }, + ); + } + + const contentType = response.headers.get('content-type') || 'application/octet-stream'; + const body = await response.arrayBuffer(); + + return buildDownloadResponse( + body, + contentType, + filename, + body.byteLength, + ); + } catch (err) { + const msg = err instanceof Error ? err.message : '下载失败'; + console.error('[Download Proxy Error]', msg); + return NextResponse.json({ error: `下载失败: ${msg}` }, { status: 502 }); + } +} + +function getLocalStorageKey(url: string): string | null { + let pathname = url; + if (url.startsWith('http://') || url.startsWith('https://')) { + try { + pathname = new URL(url).pathname; + } catch { + return null; + } + } + + const prefix = '/api/local-storage/'; + if (!pathname.startsWith(prefix)) return null; + + try { + const key = decodeURIComponent(pathname.slice(prefix.length)); + const normalized = path.posix.normalize(key).replace(/^\/+/, ''); + if (!normalized || normalized.startsWith('..') || normalized.includes('/../')) { + return null; + } + return normalized; + } catch { + return null; + } +} + +function resolveDownloadUrl(url: string, origin: string): string | null { + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + if (url.startsWith('/') && !url.startsWith('//')) { + return `${origin}${url}`; + } + + return null; +} + +function downloadLocalStorageFile(key: string, filename: string) { + if (!localStorage.fileExists(key)) { + return NextResponse.json({ error: '文件不存在' }, { status: 404 }); + } + + const fileBuffer = localStorage.readFile(key); + const contentType = getContentType(key); + + return buildDownloadResponse( + fileBuffer.buffer.slice( + fileBuffer.byteOffset, + fileBuffer.byteOffset + fileBuffer.byteLength, + ) as ArrayBuffer, + contentType, + filename, + fileBuffer.byteLength, + ); +} + +function buildDownloadResponse( + body: ArrayBuffer, + contentType: string, + filename: string, + length: number, +) { + return new NextResponse(body, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': String(length), + 'Cache-Control': 'no-cache', + }, + }); +} + +function sanitizeFilename(filename: string): string { + return path.basename(filename).replace(/[\r\n"]/g, '_') || 'download'; +} + +function getContentType(filePath: string): string { + const extension = filePath.split('.').pop()?.toLowerCase(); + const contentTypeMap: Record = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp', + gif: 'image/gif', + mp4: 'video/mp4', + avi: 'video/x-msvideo', + mov: 'video/quicktime', + wmv: 'video/x-ms-wmv', + webm: 'video/webm', + }; + + return contentTypeMap[extension || ''] || 'application/octet-stream'; +} diff --git a/src/app/api/email/reset-password/route.ts b/src/app/api/email/reset-password/route.ts new file mode 100644 index 0000000..2f91760 --- /dev/null +++ b/src/app/api/email/reset-password/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ensureEmailSchema, getRequestBaseUrl, isValidEmail, normalizeEmail, sendTemplatedEmail, verifyEmailCode } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +function passwordStrongEnough(value: string): boolean { + return value.length >= 8 && /[a-zA-Z]/.test(value) && /\d/.test(value); +} + +function friendlyError(error: unknown) { + return error instanceof Error ? error.message : '密码重置失败,请稍后再试'; +} + +export async function POST(request: NextRequest) { + const client = await getDbClient(); + try { + await ensureEmailSchema(client); + const body = await request.json(); + const email = normalizeEmail(body.email); + const code = typeof body.code === 'string' ? body.code.trim() : ''; + const newPassword = typeof body.newPassword === 'string' ? body.newPassword : ''; + + if (!isValidEmail(email) || !/^[a-z0-9]{4,10}$/i.test(code)) { + return NextResponse.json({ error: '邮箱或验证码格式不正确' }, { status: 400 }); + } + if (!passwordStrongEnough(newPassword)) { + return NextResponse.json({ error: '新密码至少 8 位,并同时包含字母和数字' }, { status: 400 }); + } + + await client.query('BEGIN'); + await verifyEmailCode(client, { email, type: 'reset_password', code }); + + const user = await client.query( + `SELECT p.id, p.nickname + FROM profiles p + JOIN auth.users u ON u.id = p.id + WHERE LOWER(p.email) = LOWER($1) AND p.email_verified = true + LIMIT 1`, + [email], + ); + if (user.rows.length === 0) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: '该邮箱尚未绑定或未完成验证' }, { status: 400 }); + } + + await client.query( + `UPDATE auth.users + SET password_hash = crypt($1, gen_salt('bf')) + WHERE id = $2`, + [newPassword, user.rows[0].id], + ); + await client.query('COMMIT'); + + await sendTemplatedEmail(client, { + to: email, + type: 'password_reset_success', + subject: '【妙境】密码已重置', + title: '密码重置成功', + intro: '你的妙境账号密码已成功重置。请使用新密码重新登录。', + note: '若非本人操作,请立即联系管理员并检查账号安全。', + assetBaseUrl: getRequestBaseUrl(request) || undefined, + }).catch(() => undefined); + + return NextResponse.json({ success: true, message: '密码已重置,请重新登录' }); + } catch (error) { + await client.query('ROLLBACK').catch(() => undefined); + return NextResponse.json({ error: friendlyError(error) }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/email/send-notification/route.ts b/src/app/api/email/send-notification/route.ts new file mode 100644 index 0000000..7833eef --- /dev/null +++ b/src/app/api/email/send-notification/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getRequestBaseUrl, isValidEmail, normalizeEmail, sendTemplatedEmail, type EmailMessageType } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +const ALLOWED_TYPES: EmailMessageType[] = [ + 'register_success', + 'email_verified', + 'password_reset_success', + 'security_login', + 'announcement', + 'order', + 'business', +]; + +export async function POST(request: NextRequest) { + const adminError = await requireAdmin(request); + if (adminError) return adminError; + + const client = await getDbClient(); + try { + const body = await request.json(); + const to = normalizeEmail(body.to); + const type = ALLOWED_TYPES.includes(body.type) ? body.type : 'business'; + const title = typeof body.title === 'string' ? body.title.trim().slice(0, 120) : ''; + const bodyText = typeof body.body === 'string' ? body.body.trim().slice(0, 4000) : ''; + const buttonText = typeof body.buttonText === 'string' ? body.buttonText.trim().slice(0, 40) : ''; + const buttonUrl = typeof body.buttonUrl === 'string' ? body.buttonUrl.trim().slice(0, 500) : ''; + + if (!isValidEmail(to)) { + return NextResponse.json({ error: '请输入正确的收件邮箱' }, { status: 400 }); + } + if (!title || !bodyText) { + return NextResponse.json({ error: '请填写邮件标题和正文' }, { status: 400 }); + } + if (buttonUrl && !/^https?:\/\/[^\s"'<>]+$/i.test(buttonUrl)) { + return NextResponse.json({ error: '按钮链接必须是 HTTP(S) 地址' }, { status: 400 }); + } + + await sendTemplatedEmail(client, { + to, + type, + subject: `【妙境】${title}`, + title, + body: bodyText, + buttonText: buttonText || undefined, + buttonUrl: buttonUrl || undefined, + note: '这是一封系统通知邮件,请勿直接回复。', + ipAddress: 'admin', + assetBaseUrl: getRequestBaseUrl(request) || undefined, + }); + + return NextResponse.json({ success: true, message: '邮件已发送' }); + } catch (error) { + const message = error instanceof Error ? error.message : '邮件发送失败'; + return NextResponse.json({ error: message }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/email/send-profile-code/route.ts b/src/app/api/email/send-profile-code/route.ts new file mode 100644 index 0000000..7260926 --- /dev/null +++ b/src/app/api/email/send-profile-code/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ensureEmailSchema, isValidEmail, normalizeEmail, sendVerificationCode } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; + +export const runtime = 'nodejs'; + +function friendlyError(error: unknown) { + return error instanceof Error ? error.message : '验证码发送失败,请稍后再试'; +} + +export async function POST(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) { + return NextResponse.json({ error: '请先登录后再验证邮箱' }, { status: 401 }); + } + + const client = await getDbClient(); + try { + await ensureEmailSchema(client); + const body = await request.json(); + const email = normalizeEmail(body.email); + if (!isValidEmail(email)) { + return NextResponse.json({ error: '请输入正确的邮箱地址' }, { status: 400 }); + } + + const user = await client.query('SELECT id, email FROM profiles WHERE id = $1 LIMIT 1', [userId]); + if (user.rows.length === 0) { + return NextResponse.json({ error: '账号不存在,请重新登录' }, { status: 404 }); + } + + const duplicate = await client.query( + 'SELECT id FROM profiles WHERE LOWER(email) = LOWER($1) AND id <> $2 LIMIT 1', + [email, userId], + ); + if (duplicate.rows.length > 0) { + return NextResponse.json({ error: '该邮箱已被其他账号绑定' }, { status: 400 }); + } + + const result = await sendVerificationCode(client, request, { email, type: 'verify_email', userId }); + return NextResponse.json({ ...result, message: '验证码已发送,请查收邮箱' }); + } catch (error) { + return NextResponse.json({ error: friendlyError(error) }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/email/send-register-code/route.ts b/src/app/api/email/send-register-code/route.ts new file mode 100644 index 0000000..118d703 --- /dev/null +++ b/src/app/api/email/send-register-code/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { sendVerificationCode, normalizeEmail, isValidEmail } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +function friendlyError(error: unknown) { + return error instanceof Error ? error.message : '验证码发送失败,请稍后再试'; +} + +export async function POST(request: NextRequest) { + const client = await getDbClient(); + try { + const body = await request.json(); + const email = normalizeEmail(body.email); + + if (!isValidEmail(email)) { + return NextResponse.json({ error: '请输入正确的邮箱地址' }, { status: 400 }); + } + + const existing = await client.query( + 'SELECT id FROM profiles WHERE LOWER(email) = LOWER($1) LIMIT 1', + [email], + ); + if (existing.rows.length > 0) { + return NextResponse.json({ error: '该邮箱已注册,请直接登录' }, { status: 400 }); + } + + const result = await sendVerificationCode(client, request, { email, type: 'register' }); + return NextResponse.json({ ...result, message: '验证码已发送,请查收邮箱' }); + } catch (error) { + return NextResponse.json({ error: friendlyError(error) }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/email/send-reset-code/route.ts b/src/app/api/email/send-reset-code/route.ts new file mode 100644 index 0000000..18d60c5 --- /dev/null +++ b/src/app/api/email/send-reset-code/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ensureEmailSchema, isValidEmail, normalizeEmail, sendVerificationCode } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; + +export const runtime = 'nodejs'; + +export async function POST(request: NextRequest) { + const client = await getDbClient(); + try { + await ensureEmailSchema(client); + const body = await request.json(); + const email = normalizeEmail(body.email); + if (!isValidEmail(email)) { + return NextResponse.json({ error: '请输入正确的邮箱地址' }, { status: 400 }); + } + + const user = await client.query( + `SELECT p.id + FROM profiles p + JOIN auth.users u ON u.id = p.id + WHERE LOWER(p.email) = LOWER($1) AND p.email_verified = true AND u.password_hash IS NOT NULL + LIMIT 1`, + [email], + ); + + if (user.rows.length > 0) { + try { + await sendVerificationCode(client, request, { + email, + type: 'reset_password', + userId: user.rows[0].id, + }); + } catch (error) { + const message = error instanceof Error ? error.message : '验证码发送失败,请稍后再试'; + return NextResponse.json({ error: message }, { status: 400 }); + } + } + + return NextResponse.json({ + success: true, + cooldown: 60, + message: '如果该邮箱已绑定并验证,我们已发送重置验证码', + }); + } finally { + client.release(); + } +} diff --git a/src/app/api/email/verify-profile/route.ts b/src/app/api/email/verify-profile/route.ts new file mode 100644 index 0000000..1b9d1ee --- /dev/null +++ b/src/app/api/email/verify-profile/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ensureEmailSchema, getRequestBaseUrl, isValidEmail, normalizeEmail, sendTemplatedEmail, verifyEmailCode } from '@/lib/email-service'; +import { getDbClient } from '@/storage/database/local-db'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; + +export const runtime = 'nodejs'; + +function friendlyError(error: unknown) { + return error instanceof Error ? error.message : '邮箱验证失败,请稍后再试'; +} + +export async function POST(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) { + return NextResponse.json({ error: '请先登录后再验证邮箱' }, { status: 401 }); + } + + const client = await getDbClient(); + try { + await ensureEmailSchema(client); + const body = await request.json(); + const email = normalizeEmail(body.email); + const code = typeof body.code === 'string' ? body.code.trim() : ''; + if (!isValidEmail(email) || !/^[a-z0-9]{4,10}$/i.test(code)) { + return NextResponse.json({ error: '邮箱或验证码格式不正确' }, { status: 400 }); + } + + await client.query('BEGIN'); + await verifyEmailCode(client, { email, type: 'verify_email', code }); + + const duplicate = await client.query( + 'SELECT id FROM profiles WHERE LOWER(email) = LOWER($1) AND id <> $2 LIMIT 1', + [email, userId], + ); + if (duplicate.rows.length > 0) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: '该邮箱已被其他账号绑定' }, { status: 400 }); + } + + const domain = email.includes('@') ? email.split('@')[1] : null; + const profile = await client.query( + `UPDATE profiles + SET email = $1, + email_verified = true, + email_verified_at = NOW(), + email_bound_at = COALESCE(email_bound_at, NOW()), + email_sender_domain = $2, + updated_at = NOW() + WHERE id = $3 + RETURNING id, email, nickname, phone, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit, avatar_url, created_at, email_verified, email_verified_at, email_bound_at`, + [email, domain, userId], + ); + await client.query('UPDATE auth.users SET email = $1 WHERE id = $2', [email, userId]); + await client.query('COMMIT'); + + await sendTemplatedEmail(client, { + to: email, + type: 'email_verified', + subject: '【妙境】邮箱验证成功', + title: '邮箱验证成功', + intro: '你的账号邮箱已完成验证,后续可用于找回密码和安全通知。', + note: '若非本人操作,请尽快修改账号密码。', + assetBaseUrl: getRequestBaseUrl(request) || undefined, + }).catch(() => undefined); + + return NextResponse.json({ success: true, profile: profile.rows[0], message: '邮箱验证成功' }); + } catch (error) { + await client.query('ROLLBACK').catch(() => undefined); + return NextResponse.json({ error: friendlyError(error) }, { status: 400 }); + } finally { + client.release(); + } +} diff --git a/src/app/api/gallery/publish/route.ts b/src/app/api/gallery/publish/route.ts new file mode 100644 index 0000000..4c45a8e --- /dev/null +++ b/src/app/api/gallery/publish/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { localStorage } from '@/lib/local-storage'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; + +export async function POST(request: NextRequest) { + try { + const tokenUserId = await getAuthenticatedUserId(request); + if (!tokenUserId) { + return NextResponse.json({ error: '请先登录后再发布作品' }, { status: 401 }); + } + + const body = await request.json(); + const { + userId, + type, + prompt, + negativePrompt, + resultUrl, + thumbnailUrl, + width, + height, + duration, + params, + model, + modelLabel, + creditsCost, + } = body; + + if (!resultUrl) { + return NextResponse.json({ error: '缺少作品 URL' }, { status: 400 }); + } + + const client = await getDbClient(); + + try { + const profileResult = await client.query( + 'SELECT id FROM profiles WHERE id = $1 AND is_active = true LIMIT 1', + [tokenUserId], + ); + if (profileResult.rows.length === 0) { + return NextResponse.json({ error: '发布用户不存在或已停用' }, { status: 403 }); + } + + const hasReference = Boolean(body.referenceImage) + || (Array.isArray(body.referenceImages) && body.referenceImages.length > 0) + || (Array.isArray((params as Record | undefined)?.referenceImages) && ((params as Record).referenceImages as unknown[]).length > 0); + const explicitMode = (params as Record | undefined)?.creationMode || body.creationMode; + const workType = explicitMode === 'text2img' || explicitMode === 'img2img' || explicitMode === 'text2video' || explicitMode === 'img2video' + ? explicitMode + : type === 'video' ? (hasReference ? 'img2video' : 'text2video') + : type === 'image' ? (hasReference ? 'img2img' : 'text2img') + : type; + + const safeUserId = tokenUserId; + + const id = crypto.randomUUID(); + let galleryResultUrl = resultUrl; + let galleryThumbnailUrl = thumbnailUrl || null; + try { + const folder = type === 'video' ? 'gallery/videos' : 'gallery/images'; + galleryResultUrl = await localStorage.copyPublicUrlToFolder(resultUrl, folder); + if (thumbnailUrl) { + galleryThumbnailUrl = await localStorage.copyPublicUrlToFolder(thumbnailUrl, 'gallery/thumbnails'); + } + } catch (copyError) { + console.warn('[gallery/publish] copy to gallery folder failed, using original URL:', copyError); + } + + await client.query( + `INSERT INTO works (id, user_id, type, title, prompt, negative_prompt, result_url, thumbnail_url, width, height, duration, is_public, likes_count, credits_cost, status, params) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, true, 0, $12, 'completed', $13)`, + [ + id, + safeUserId, + workType, + body.title || null, + prompt || null, + negativePrompt || null, + galleryResultUrl, + galleryThumbnailUrl, + width || null, + height || null, + duration || null, + creditsCost || 0, + JSON.stringify({ + ...((params as Record) || {}), + model, + modelLabel, + referenceImage: body.referenceImage || undefined, + referenceImages: body.referenceImages || undefined, + }), + ] + ); + + return NextResponse.json({ success: true, workId: id, resultUrl: galleryResultUrl }); + } finally { + client.release(); + } + } catch (err) { + console.error('[gallery/publish] POST error:', err); + return NextResponse.json({ error: '服务器错误' }, { status: 500 }); + } +} + + diff --git a/src/app/api/gallery/route.ts b/src/app/api/gallery/route.ts new file mode 100644 index 0000000..10627ac --- /dev/null +++ b/src/app/api/gallery/route.ts @@ -0,0 +1,152 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/admin-auth'; +import { getDbClient } from '@/storage/database/local-db'; + +function getReferenceImages(params: Record) { + const referenceImages = Array.isArray(params.referenceImages) + ? params.referenceImages.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) + : []; + const referenceImage = typeof params.referenceImage === 'string' && params.referenceImage.trim() + ? params.referenceImage + : referenceImages[0]; + return { referenceImage, referenceImages }; +} + +export async function GET(request: NextRequest) { + const url = request.nextUrl.searchParams; + const type = url.get('type'); + const limit = Math.min(parseInt(url.get('limit') || '50', 10), 300); + const offset = parseInt(url.get('offset') || '0', 10); + const sort = url.get('sort') || 'newest'; + const search = (url.get('q') || url.get('search') || '').trim().toLowerCase(); + + try { + const client = await getDbClient(); + + try { + const where: string[] = ['w.is_public = true', 'w.status = $1']; + const params: unknown[] = ['completed']; + + if (type === 'image') { + params.push('text2img', 'img2img'); + where.push(`w.type IN ($${params.length - 1}, $${params.length})`); + } else if (type === 'video') { + params.push('text2video', 'img2video'); + where.push(`w.type IN ($${params.length - 1}, $${params.length})`); + } + + if (search) { + params.push(`%${search}%`); + const idx = params.length; + where.push(`( + LOWER(COALESCE(w.title, '')) LIKE $${idx} + OR LOWER(COALESCE(w.prompt, '')) LIKE $${idx} + OR LOWER(COALESCE(w.negative_prompt, '')) LIKE $${idx} + OR LOWER(COALESCE(p.nickname, '')) LIKE $${idx} + OR LOWER(COALESCE(p.email, '')) LIKE $${idx} + OR LOWER(COALESCE(w.params::text, '')) LIKE $${idx} + )`); + } + + let query = ` + SELECT w.id, w.type, w.title, w.prompt, w.negative_prompt, w.result_url, w.thumbnail_url, + w.width, w.height, w.duration, w.is_public, w.likes_count, w.credits_cost, + w.status, w.created_at, w.user_id, w.params, + p.nickname, p.email, p.avatar_url + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE ${where.join(' AND ')} + `; + + if (sort === 'popular') { + query += ' ORDER BY w.likes_count DESC, w.created_at DESC'; + } else { + query += ' ORDER BY w.created_at DESC'; + } + + query += ` LIMIT ${limit} OFFSET ${offset}`; + + const result = await client.query(query, params); + const countResult = await client.query( + `SELECT COUNT(*) as total + FROM works w + LEFT JOIN profiles p ON p.id = w.user_id + WHERE ${where.join(' AND ')}`, + params, + ); + + const works = (result.rows || []).map((w: Record) => { + const workParams = (w.params || {}) as Record; + const references = getReferenceImages(workParams); + return { + id: w.id, + type: w.type, + title: w.title, + prompt: w.prompt, + negativePrompt: w.negative_prompt, + url: w.result_url, + thumbnailUrl: w.thumbnail_url, + width: w.width, + height: w.height, + duration: w.duration, + likes: w.likes_count || 0, + creditsCost: w.credits_cost || 0, + params: workParams, + referenceImage: references.referenceImage, + referenceImages: references.referenceImages, + publisherId: w.user_id, + publisherNickname: (w.nickname as string) || ((w.email as string) || '').split('@')[0] || '匿名用户', + publisherAvatarUrl: (w.avatar_url as string | null) || null, + publishedAt: w.created_at, + }; + }); + + return NextResponse.json({ works, total: parseInt(countResult.rows[0]?.total || '0', 10) }); + } finally { + client.release(); + } + } catch (err) { + console.error('[gallery] GET error:', err); + return NextResponse.json({ error: '获取作品列表失败' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json().catch(() => ({})); + const searchId = request.nextUrl.searchParams.get('id'); + const bodyIds = Array.isArray(body.ids) ? body.ids : []; + const ids = [...new Set([searchId, ...bodyIds].filter((id): id is string => typeof id === 'string' && id.trim().length > 0))]; + + if (ids.length === 0) { + return NextResponse.json({ error: '缺少要删除的作品 ID' }, { status: 400 }); + } + if (ids.length > 100) { + return NextResponse.json({ error: '单次最多删除 100 个画廊作品' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + const result = await client.query( + `UPDATE works + SET is_public = false + WHERE id = ANY($1) AND is_public = true + RETURNING id`, + [ids], + ); + return NextResponse.json({ + success: true, + removed: result.rowCount || 0, + ids: result.rows.map((row: Record) => row.id), + }); + } finally { + client.release(); + } + } catch (err) { + console.error('[gallery] DELETE error:', err); + return NextResponse.json({ error: '删除画廊作品失败' }, { status: 500 }); + } +} diff --git a/src/app/api/generate/image/route.ts b/src/app/api/generate/image/route.ts new file mode 100644 index 0000000..fd0970a --- /dev/null +++ b/src/app/api/generate/image/route.ts @@ -0,0 +1,1033 @@ +import { NextRequest, NextResponse } from 'next/server'; +import sharp from 'sharp'; +import { ImageGenerationClient, Config, HeaderUtils } from 'coze-coding-dev-sdk'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError, parseCustomApiJsonWithProgress } from '@/lib/custom-api-fetch'; +import { getAspectRatioPromptHint, resolveImageSize } from '@/lib/model-config'; +import { localStorage } from '@/lib/local-storage'; +import { fetchPublicHttpUrl } from '@/lib/remote-fetch'; +import { isTrustedInternalGenerationRequest, isUuid, resolveServerApiConfig } from '@/lib/server-api-config'; +import { updateGenerationJobProgress } from '@/lib/generation-job-estimates'; +import { + compressImageBufferForUpstream, + dataUrlToImageBuffer, + imageBufferToDataUrl, +} from '@/lib/server-image-compression'; + +interface CustomApiConfig { + apiUrl: string; + modelName: string; + apiKey: string; + provider: string; + customApiKeyId?: string; + systemApiId?: string; +} + +const GENERATION_TIMEOUT = 300_000; +const GENERATION_TIMEOUT_SECONDS = GENERATION_TIMEOUT / 1000; +const MAX_UPSTREAM_REFERENCE_IMAGE_BYTES = Number(process.env.MAX_UPSTREAM_REFERENCE_IMAGE_BYTES || 700 * 1024); + +interface TargetImageSize { + width: number; + height: number; +} + +interface PersistedImageResult { + url: string; + width: number; + height: number; + bytes: number; +} + +export const runtime = 'nodejs'; + +function parseImageSize(size: string | undefined): TargetImageSize | null { + const match = size?.match(/^(\d{2,5})x(\d{2,5})$/i); + if (!match) return null; + const width = Number(match[1]); + const height = Number(match[2]); + return width > 0 && height > 0 ? { width, height } : null; +} + +function resolveTargetImageSize( + size: string | undefined, + aspectRatio: string | undefined, + resolution: string | undefined, + quality: string | undefined, +): TargetImageSize | null { + const explicit = parseImageSize(size); + if (explicit) return explicit; + + if (aspectRatio && aspectRatio !== 'original' && resolution) { + return parseImageSize(resolveImageSize(aspectRatio, resolution)); + } + + const squareByQuality: Record = { + '1K': '1024x1024', + '1080P': '1024x1024', + '2K': '2048x2048', + '4K': '4096x4096', + }; + return parseImageSize(quality ? squareByQuality[quality] : undefined); +} + +function formatTargetSize(targetSize: TargetImageSize): string { + return `${targetSize.width}x${targetSize.height}`; +} + +function imageMeetsTargetSize(width: number, height: number, targetSize: TargetImageSize): boolean { + return width >= targetSize.width && height >= targetSize.height; +} + +function getImageExtension(mimeType: string | null | undefined, fallbackUrl?: string): string { + const normalized = mimeType?.split(';')[0].trim().toLowerCase(); + const mimeExt: Record = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/webp': 'webp', + }; + if (normalized && mimeExt[normalized]) return mimeExt[normalized]; + const urlExt = fallbackUrl?.split('?')[0].match(/\.([a-z0-9]+)$/i)?.[1]; + return urlExt || 'png'; +} + +function parseImageDataUrl(dataUrl: string): { buffer: Buffer; mimeType: string; ext: string } | null { + const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/); + if (!match) return null; + const [, mimeType, base64Data] = match; + return { + buffer: Buffer.from(base64Data, 'base64'), + mimeType, + ext: getImageExtension(mimeType), + }; +} + +async function persistImageWithMetadata(url: string, prefix: string): Promise { + let buffer: Buffer; + let mimeType = 'image/png'; + let ext = 'png'; + + if (url.startsWith('data:')) { + const parsed = parseImageDataUrl(url); + if (!parsed) return null; + buffer = parsed.buffer; + mimeType = parsed.mimeType; + ext = parsed.ext; + } else { + const existingKey = localStorage.getKeyFromPublicUrl(url); + if (existingKey && localStorage.fileExists(existingKey)) { + buffer = localStorage.readFile(existingKey); + ext = existingKey.split('.').pop() || ext; + } else if (url.startsWith('http')) { + const response = await withTimeout(fetchPublicHttpUrl(url), 30_000, 'Fetch generated image'); + if (!response.ok) throw new Error(`下载生成图片失败: ${response.status}`); + mimeType = response.headers.get('content-type')?.split(';')[0] || mimeType; + buffer = Buffer.from(await response.arrayBuffer()); + ext = getImageExtension(mimeType, url); + } else { + return null; + } + } + + const metadata = await sharp(buffer, { failOn: 'none' }).metadata(); + if (!metadata.width || !metadata.height) { + throw new Error('无法读取生成图片尺寸'); + } + + const fileName = `${prefix}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + const fileKey = await withTimeout( + localStorage.uploadFile({ + fileContent: buffer, + fileName, + contentType: mimeType, + }), + 30_000, + 'Local uploadFile', + ); + const presignedUrl = await withTimeout( + localStorage.generatePresignedUrl({ key: fileKey, expireTime: 2592000 }), + 10_000, + 'Local generatePresignedUrl', + ); + + return { + url: presignedUrl, + width: metadata.width, + height: metadata.height, + bytes: buffer.length, + }; +} + +async function persistQualifiedImageUrls( + urls: string[], + prefix: string, + targetSize: TargetImageSize | null, + context: string, +): Promise<{ images: string[]; rejected: string[] }> { + const images: string[] = []; + const rejected: string[] = []; + + for (const url of urls) { + try { + const persisted = await persistImageWithMetadata(url, prefix); + if (!persisted) { + rejected.push('无法读取生成图片'); + continue; + } + if (targetSize && !imageMeetsTargetSize(persisted.width, persisted.height, targetSize)) { + const message = `${persisted.width}x${persisted.height} < ${formatTargetSize(targetSize)}`; + console.warn(`[${context}] Rejected low-resolution image:`, message); + rejected.push(message); + continue; + } + console.log(`[${context}] Accepted generated image:`, `${persisted.width}x${persisted.height}`, 'bytes:', persisted.bytes); + images.push(persisted.url); + } catch (err) { + const message = err instanceof Error ? err.message : '图片处理失败'; + console.warn(`[${context}] Failed to persist generated image:`, message); + rejected.push(message); + } + } + + return { images, rejected }; +} + +async function fetchCustomImageGeneration( + endpoint: string, + apiKey: string, + requestBody: Record, + onProgress?: (progress: Record) => void | Promise, +): Promise<{ ok: true; images: string[] } | { ok: false; response: Response; errorText: string }> { + const response = await fetchWithRetry( + endpoint, + { method: 'POST', headers: buildCustomApiHeaders(apiKey), body: JSON.stringify(requestBody) }, + GENERATION_TIMEOUT, + 1, + ); + + if (!response.ok) { + return { ok: false, response, errorText: await response.text() }; + } + + const data = await parseCustomApiJsonWithProgress(response, onProgress); + return { ok: true, images: extractImagesFromGenerationsResponse(data as Record) }; +} + +async function requestQualifiedCustomImages( + endpoint: string, + apiKey: string, + requestBody: Record, + targetCount: number, + targetSize: TargetImageSize | null, + onProgress?: (progress: Record) => void | Promise, +): Promise<{ images: string[]; rejected: string[]; upstreamError?: { status: number; text: string } }> { + const accepted: string[] = []; + const rejected: string[] = []; + const maxAttempts = Math.max(targetCount * 3, 3); + + for (let attempt = 1; attempt <= maxAttempts && accepted.length < targetCount; attempt += 1) { + const remaining = targetCount - accepted.length; + const requestCount = attempt === 1 + ? Math.max(remaining, Number(requestBody.n) || 1) + : 1; + const response = await fetchCustomImageGeneration( + endpoint, + apiKey, + { ...requestBody, n: requestCount }, + onProgress, + ); + + if (!response.ok) { + return { + images: accepted, + rejected, + upstreamError: { status: response.response.status, text: response.errorText }, + }; + } + + if (response.images.length === 0) { + rejected.push('响应中无图片数据'); + continue; + } + + const persisted = await persistQualifiedImageUrls( + response.images, + 'generated/images', + targetSize, + `Custom API Image attempt ${attempt}`, + ); + accepted.push(...persisted.images); + rejected.push(...persisted.rejected); + } + + return { images: accepted.slice(0, targetCount), rejected }; +} + +function lowResolutionError(targetSize: TargetImageSize | null, rejected: string[]): string { + const target = targetSize ? `要求 ${formatTargetSize(targetSize)}` : '要求的分辨率'; + const actual = rejected.length > 0 ? `,实际返回:${rejected.join(';')}` : ''; + return `上游返回图片分辨率不符合${target}${actual}`; +} + +/** Helper: wrap a promise with a timeout that rejects with a descriptive message */ +function withTimeout(promise: Promise, ms: number, label: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + promise.then( + (v) => { clearTimeout(timer); resolve(v); }, + (e) => { clearTimeout(timer); reject(e); }, + ); + }); +} + +/** + * Upload a base64 data URL to S3 storage and return a presigned URL. + */ +async function uploadDataUrlAndGetPublicUrl(dataUrl: string): Promise { + try { + const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/); + if (!match) return null; + const [, mimeType, base64Data] = match; + const ext = mimeType.split('/')[1] || 'png'; + const buffer = Buffer.from(base64Data, 'base64'); + const fileName = `img2img-ref/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + + const fileKey = await localStorage.uploadFile({ + fileContent: buffer, + fileName, + contentType: mimeType, + }); + + if (!fileKey) { + console.error('[Upload Ref Image] uploadFile returned empty key'); + return null; + } + + const presignedUrl = await localStorage.generatePresignedUrl({ + key: fileKey, + expireTime: 3600, + }); + + console.log('[Upload Ref Image] Success, key:', fileKey, 'url length:', presignedUrl?.length); + return presignedUrl || null; + } catch (err) { + console.error('[Upload Ref Image Error]', err instanceof Error ? err.message : err); + return null; + } +} + +/** + * Derive the chat completions endpoint URL from an images/generations URL. + */ +function deriveChatCompletionsUrl(imagesUrl: string): string { + if (imagesUrl.includes('/chat/completions')) return imagesUrl; + return imagesUrl + .replace(/\/images\/(generations|edits).*/i, '/chat/completions') + .replace(/\/+$/, ''); +} + +/** + * Derive the images/edits endpoint URL from an images/generations URL. + * This is the official OpenAI endpoint for image-to-image. + */ +function deriveImagesEditsUrl(imagesUrl: string): string { + if (imagesUrl.includes('/images/edits')) return imagesUrl; + return imagesUrl + .replace(/\/images\/generations.*/i, '/images/edits') + .replace(/\/+$/, ''); +} + +/** + * Extract image URLs/data from a chat completions response. + */ +function extractImagesFromChatResponse(data: Record): string[] { + const images: string[] = []; + const choices = data.choices as Array> | undefined; + if (Array.isArray(choices)) { + for (const choice of choices) { + const message = choice.message as Record | undefined; + if (!message) continue; + const content = message.content; + + if (typeof content === 'string') { + if (content.startsWith('data:image/') || content.startsWith('http')) { + images.push(content); + } + const mdMatch = content.match(/!\[.*?\]\((data:image\/[^)]+)\)/); + if (mdMatch) images.push(mdMatch[1]); + const urlMatch = content.match(/(https?:\/\/[^\s"']+\.(png|jpg|jpeg|webp)[^\s"']*)/i); + if (urlMatch) images.push(urlMatch[1]); + } else if (Array.isArray(content)) { + for (const item of content as Array>) { + if (item.type === 'image_url' && item.image_url) { + const url = (item.image_url as Record).url; + if (typeof url === 'string') images.push(url); + } + if (item.type === 'image' && item.image) { + const imgData = item.image as Record; + if (typeof imgData.url === 'string') images.push(imgData.url); + if (typeof imgData.b64_json === 'string') { + images.push(`data:image/png;base64,${imgData.b64_json}`); + } + } + if (item.type === 'text' && typeof item.text === 'string') { + const text = item.text as string; + if (text.startsWith('data:image/')) images.push(text); + if (text.startsWith('http') && /\.(png|jpg|jpeg|webp)/i.test(text)) images.push(text); + const mdMatch = text.match(/!\[.*?\]\((data:image\/[^)]+)\)/); + if (mdMatch) images.push(mdMatch[1]); + const urlMatch = text.match(/(https?:\/\/[^\s"']+\.(png|jpg|jpeg|webp)[^\s"']*)/i); + if (urlMatch) images.push(urlMatch[1]); + } + } + } + } + } + return images; +} + +function objectKeysFromUnknown(value: unknown): string[] { + if (!value || typeof value !== 'object' || Array.isArray(value)) return []; + return Object.keys(value); +} + +/** + * Extract images from images/generations or images/edits response format. + */ +function extractImagesFromGenerationsResponse(data: Record): string[] { + const images: string[] = []; + if (Array.isArray(data.data)) { + for (const item of data.data as Array>) { + if (typeof item === 'string') { images.push(item); continue; } + if (item.b64_json && typeof item.b64_json === 'string') { + images.push(`data:image/png;base64,${item.b64_json}`); + } + if (item.url && typeof item.url === 'string') images.push(item.url); + } + } else if (typeof data.url === 'string') { + images.push(data.url); + } else if (typeof data.image_url === 'string') { + images.push(data.image_url); + } + return images; +} + +/** Track which strategy produced a result */ +interface StrategyResult { + success: boolean; + images?: string[]; + error?: string; + status?: number; + strategyName: string; +} + +/** + * Try a single API request strategy and return the result. + */ +async function tryImageStrategy( + url: string, + headers: Record, + body: Record, + strategyName: string, + isChatFormat: boolean, + onProgress?: (progress: Record) => void | Promise, +): Promise { + console.log(`[Custom API img2img → ${strategyName}] URL:`, url, + '| model:', body.model, + '| body_keys:', Object.keys(body).join(',')); + + try { + const response = await fetchWithRetry( + url, + { + method: 'POST', + headers, + body: JSON.stringify(body), + }, + GENERATION_TIMEOUT, + 1, + ); + + if (response.ok) { + const data = await parseCustomApiJsonWithProgress(response, onProgress); + let images = isChatFormat + ? extractImagesFromChatResponse(data as Record) + : []; + if (images.length === 0) { + images = extractImagesFromGenerationsResponse(data as Record); + } + + if (images.length > 0) { + console.log(`[Custom API img2img → ${strategyName} SUCCESS] Got`, images.length, 'images'); + return { success: true, images, strategyName }; + } + + console.warn(`[Custom API img2img → ${strategyName}] OK but no images extracted, keys:`, objectKeysFromUnknown(data)); + return { success: false, error: '响应中无图片数据', strategyName }; + } + + const errorText = await response.text(); + console.warn(`[Custom API img2img → ${strategyName} FAILED]`, response.status, errorText.slice(0, 200)); + return { success: false, error: parseCustomApiError(response.status, errorText), status: response.status, strategyName }; + } catch (err) { + const msg = err instanceof Error ? err.message : '请求异常'; + console.warn(`[Custom API img2img → ${strategyName} ERROR]`, msg); + return { success: false, error: msg, strategyName }; + } +} + +/** + * Try images/edits endpoint with multipart/form-data format. + * + * CRITICAL: This is the format Cherry Studio (Electron app) uses for img2img. + * OpenAI's official /v1/images/edits endpoint uses multipart/form-data, NOT JSON. + * API proxies like mozhevip.top route based on Content-Type: + * - multipart/form-data → routed to img2img account pool → WORKS + * - application/json → routed to wrong pool → 503 "No available compatible accounts" + * + * This is why the same API+Key works in Cherry Studio but not from our server. + */ +async function tryEditsWithFormData( + url: string, + apiKey: string, + model: string, + prompt: string, + imageBuffer: Buffer, + imageMimeType: string, + size: string | undefined, + strength: number | undefined, + count: number, + onProgress?: (progress: Record) => void | Promise, +): Promise { + const strategyName = '策略2: images/edits (FormData)'; + console.log(`[Custom API img2img → ${strategyName}] URL:`, url, '| model:', model); + + try { + // Build multipart/form-data manually (Node.js doesn't have native FormData that works with fetch) + const boundary = `----FormBoundary${Date.now()}${Math.random().toString(36).slice(2)}`; + const parts: Buffer[] = []; + + // Add text fields + const textFields: Record = { + model, + prompt, + }; + if (size) textFields.size = size; + if (count > 1) textFields.n = String(count); + if (strength !== undefined) textFields.strength = String(strength); + + for (const [key, value] of Object.entries(textFields)) { + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n` + )); + } + + // Add image file field + const ext = imageMimeType.split('/')[1] || 'png'; + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="image"; filename="image.${ext}"\r\nContent-Type: ${imageMimeType}\r\n\r\n` + )); + parts.push(imageBuffer); + parts.push(Buffer.from(`\r\n`)); + + // Close boundary + parts.push(Buffer.from(`--${boundary}--\r\n`)); + + const bodyBuffer = Buffer.concat(parts); + + const response = await fetchWithRetry( + url, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + }, + body: bodyBuffer, + }, + GENERATION_TIMEOUT, + 1, + ); + + if (response.ok) { + const data = await parseCustomApiJsonWithProgress(response, onProgress); + const images = extractImagesFromGenerationsResponse(data as Record); + if (images.length > 0) { + console.log(`[Custom API img2img → ${strategyName} SUCCESS] Got`, images.length, 'images'); + return { success: true, images, strategyName }; + } + console.warn(`[Custom API img2img → ${strategyName}] OK but no images, keys:`, objectKeysFromUnknown(data)); + return { success: false, error: '响应中无图片数据', strategyName }; + } + + const errorText = await response.text(); + console.warn(`[Custom API img2img → ${strategyName} FAILED]`, response.status, errorText.slice(0, 200)); + return { success: false, error: parseCustomApiError(response.status, errorText), status: response.status, strategyName }; + } catch (err) { + const msg = err instanceof Error ? err.message : '请求异常'; + console.warn(`[Custom API img2img → ${strategyName} ERROR]`, msg); + return { success: false, error: msg, strategyName }; + } +} + +/** + * Image-to-image via custom API with multi-strategy approach. + * Tries 3 different endpoint formats in order: + * 1. /v1/chat/completions with image_url (Cherry Studio / OpenAI multimodal style) + * 2. /v1/images/edits with image (Official OpenAI image edit endpoint) + * 3. /v1/images/generations with init_image (Reference code / Stable Diffusion style) + */ +async function customApiImageToImage( + customApiConfig: CustomApiConfig, + prompt: string, + negativePrompt: string | undefined, + image: string, + strength: number | undefined, + size: string | undefined, + count: number, + targetSize: TargetImageSize | null, + aspectRatio?: string, + onProgress?: (progress: Record) => void | Promise, +): Promise { + const endpoint = customApiConfig.apiUrl; + if (!endpoint) { + return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 }); + } + if (!customApiConfig.modelName) { + return NextResponse.json({ error: '自定义API未配置模型名称,请在设置中填写模型名称(如 gpt-image-2)' }, { status: 400 }); + } + + let normalizedImage = image; + + // Prepare image buffer for FormData upload + let imageBuffer: Buffer | null = null; + let imageMimeType = 'image/png'; + if (image.startsWith('data:')) { + const parsedImage = dataUrlToImageBuffer(image); + if (parsedImage) { + imageMimeType = parsedImage.mimeType; + imageBuffer = parsedImage.buffer; + } + } else { + // It's a URL - download it first + try { + const imgRes = await fetchPublicHttpUrl(image); + if (imgRes.ok) { + const contentType = imgRes.headers.get('content-type') || 'image/png'; + imageMimeType = contentType.split(';')[0]; + const arrayBuf = await imgRes.arrayBuffer(); + imageBuffer = Buffer.from(arrayBuf); + } + } catch (e) { + console.warn('[Custom API img2img] Failed to download reference image from URL:', e); + } + } + + if (imageBuffer) { + try { + const compressed = await compressImageBufferForUpstream( + { buffer: imageBuffer, mimeType: imageMimeType }, + { maxBytes: MAX_UPSTREAM_REFERENCE_IMAGE_BYTES }, + ); + if (compressed.changed) { + console.log('[Custom API img2img] Compressed reference image:', compressed.originalBytes, '→', compressed.buffer.length); + } + imageBuffer = compressed.buffer; + imageMimeType = compressed.mimeType; + normalizedImage = imageBufferToDataUrl({ buffer: imageBuffer, mimeType: imageMimeType }); + } catch (err) { + console.warn('[Custom API img2img] Reference image compression failed, using original:', err instanceof Error ? err.message : err); + } + } + + // Upload reference image to S3 to get a public URL (for strategies that use URL instead of file upload) + let imageUrl = normalizedImage; + if (normalizedImage.startsWith('data:')) { + console.log('[Custom API img2img] Uploading reference image to S3 to reduce payload...'); + const uploadedUrl = await uploadDataUrlAndGetPublicUrl(normalizedImage); + if (uploadedUrl) { + imageUrl = uploadedUrl; + console.log('[Custom API img2img] Using S3 URL, size reduction:', normalizedImage.length, '→', imageUrl.length); + } else { + console.warn('[Custom API img2img] S3 upload failed, falling back to data URL in request body'); + } + } + + // Build prompt text with optional negative prompt and strength hints + let promptText = prompt; + if (negativePrompt) { + promptText += `\n\n负面提示词(排除以下元素): ${negativePrompt}`; + } + if (strength !== undefined && strength !== 0.5) { + promptText += `\n\n[重绘幅度: ${strength.toFixed(2)},${strength < 0.5 ? '尽量保留参考图特征' : '更贴近提示词描述'}]`; + } + // Augment prompt with aspect ratio hint + if (aspectRatio) { + const hint = getAspectRatioPromptHint(aspectRatio); + if (hint) promptText += `\n\n[${hint}]`; + } + + const headers = buildCustomApiHeaders(customApiConfig.apiKey); + const denoisingStrength = strength ?? 0.5; + + // --- Strategy 1: /v1/images/edits with multipart/form-data --- + // This is THE format Cherry Studio uses! OpenAI's official endpoint. + // API proxies route multipart/form-data to the correct img2img account pool. + let result1: StrategyResult | null = null; + if (imageBuffer) { + const editsUrl = deriveImagesEditsUrl(endpoint); + result1 = await tryEditsWithFormData( + editsUrl, + customApiConfig.apiKey, + customApiConfig.modelName, + promptText, + imageBuffer, + imageMimeType, + size, + denoisingStrength, + count, + onProgress, + ); + if (result1.success && result1.images) { + const persisted = await persistQualifiedImageUrls(result1.images, 'generated/images', targetSize, 'Custom API img2img strategy1'); + if (persisted.images.length > 0) return NextResponse.json({ images: persisted.images }); + result1 = { ...result1, success: false, error: lowResolutionError(targetSize, persisted.rejected) }; + } + } + + // --- Strategy 2: chat/completions with image_url (multimodal style) --- + const chatUrl = deriveChatCompletionsUrl(endpoint); + const chatBody: Record = { + model: customApiConfig.modelName, + stream: false, + messages: [ + { + role: 'user', + content: [ + { type: 'image_url', image_url: { url: imageUrl } }, + { type: 'text', text: promptText }, + ], + }, + ], + size: size || '1024x1024', + n: count, + }; + const result2 = await tryImageStrategy(chatUrl, headers, chatBody, '策略2: chat/completions', true, onProgress); + if (result2.success && result2.images) { + const persisted = await persistQualifiedImageUrls(result2.images, 'generated/images', targetSize, 'Custom API img2img strategy2'); + if (persisted.images.length > 0) return NextResponse.json({ images: persisted.images }); + result2.success = false; + result2.error = lowResolutionError(targetSize, persisted.rejected); + } + + // --- Strategy 3: /v1/images/generations with init_image (Reference code / SD style) --- + let rawBase64 = normalizedImage; + if (normalizedImage.startsWith('data:')) { + const commaIndex = normalizedImage.indexOf(','); + if (commaIndex !== -1) rawBase64 = normalizedImage.substring(commaIndex + 1); + } + + const imgBody: Record = { + model: customApiConfig.modelName, + prompt: promptText, + n: count, + size: size || '1024x1024', + response_format: 'b64_json', + init_image: rawBase64, + denoising_strength: denoisingStrength, + }; + const result3 = await tryImageStrategy(endpoint, headers, imgBody, '策略3: images/generations+init_image', false, onProgress); + if (result3.success && result3.images) { + const persisted = await persistQualifiedImageUrls(result3.images, 'generated/images', targetSize, 'Custom API img2img strategy3'); + if (persisted.images.length > 0) return NextResponse.json({ images: persisted.images }); + result3.success = false; + result3.error = lowResolutionError(targetSize, persisted.rejected); + } + + const upstreamError = result1?.error || result2.error || result3.error; + const upstreamStatus = result1?.status || result2.status || result3.status || 502; + return NextResponse.json( + { + error: upstreamError || '图生图失败', + }, + { status: upstreamStatus >= 500 ? 502 : upstreamStatus } + ); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + prompt, + negativePrompt, + model = 'doubao-seedream-5-0-260128', + quality = '2K', + size, + aspectRatio, + resolution, + count = 1, + guidanceScale = 7, + image, + strength, + customApiConfig, + } = body as { + prompt?: string; + negativePrompt?: string; + model?: string; + quality?: string; + size?: string; + aspectRatio?: string; + resolution?: string; + count?: number; + guidanceScale?: number; + image?: string; + strength?: number; + customApiConfig?: CustomApiConfig; + }; + + if (!prompt) { + return NextResponse.json({ error: '请提供创作描述' }, { status: 400 }); + } + + if (prompt.length < 2) { + return NextResponse.json({ error: '创作描述过短,请输入更详细的描述' }, { status: 400 }); + } + + const trustedInternalRequest = isTrustedInternalGenerationRequest(request); + const trustedUserId = trustedInternalRequest + ? request.headers.get('x-miaojing-generation-user-id') + : null; + const generationJobId = trustedInternalRequest + ? request.headers.get('x-miaojing-generation-job-id') + : null; + const resolvedCustomApiConfig = await resolveServerApiConfig( + request, + customApiConfig, + isUuid(trustedUserId) ? trustedUserId : null, + ); + const handleUpstreamProgress = (progress: Record) => updateGenerationJobProgress( + isUuid(generationJobId) ? generationJobId : null, + progress, + ); + const targetSize = resolveTargetImageSize(size, aspectRatio, resolution, quality); + + // Log all incoming parameters for debugging + console.log('[Image Generation] Params:', JSON.stringify({ + model, + size, + aspectRatio, + resolution, + count, + guidanceScale, + hasCustomApi: !!resolvedCustomApiConfig, + customApiUrl: resolvedCustomApiConfig?.apiUrl, + customApiModel: resolvedCustomApiConfig?.modelName, + hasImage: !!image, + strength, + promptLength: prompt.length, + })); + + // ---- Custom API mode ---- + if (resolvedCustomApiConfig && resolvedCustomApiConfig.apiKey) { + const resolvedApiKey = resolvedCustomApiConfig.apiKey; + try { + // Image-to-image: use multi-strategy approach + if (image) { + return await customApiImageToImage( + resolvedCustomApiConfig as CustomApiConfig, + prompt, + negativePrompt, + image, + strength, + size, + count, + targetSize, + aspectRatio, + handleUpstreamProgress, + ); + } + + // Text-to-image: use images/generations format + const endpoint = resolvedCustomApiConfig.apiUrl; + if (!endpoint) { + return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 }); + } + if (!resolvedCustomApiConfig.modelName) { + return NextResponse.json({ error: '自定义API未配置模型名称,请在设置中填写模型名称(如 gpt-image-2)' }, { status: 400 }); + } + + // Ensure n is at least 1 + const n = Math.max(1, count || 1); + + // Augment prompt with aspect ratio hint as fallback + // Many APIs ignore size/aspect_ratio params, so embedding in prompt helps + const ratioHint = aspectRatio ? getAspectRatioPromptHint(aspectRatio) : ''; + const augmentedPrompt = ratioHint ? `${prompt}\n\n[${ratioHint}]` : prompt; + + const requestBody: Record = { + model: resolvedCustomApiConfig.modelName, + prompt: augmentedPrompt, + n, + size: size || '1024x1024', + response_format: 'b64_json', + }; + if (negativePrompt) { + requestBody.negative_prompt = negativePrompt; + } + // Pass guidance_scale for diffusion models (CFG scale) + if (guidanceScale && guidanceScale !== 7) { + requestBody.guidance_scale = guidanceScale; + } + // Pass aspect_ratio for APIs that prefer it over pixel size + if (aspectRatio) { + requestBody.aspect_ratio = aspectRatio; + } + + console.log('[Custom API Image] Text-to-image, sending to:', endpoint, + '| model:', requestBody.model, + '| size:', requestBody.size, + '| n:', requestBody.n, + '| aspect_ratio:', requestBody.aspect_ratio, + '| guidance_scale:', requestBody.guidance_scale, + '| prompt_length:', prompt.length, + '| augmented_prompt_length:', augmentedPrompt.length); + + let customGenerationResult: Awaited>; + try { + customGenerationResult = await requestQualifiedCustomImages( + endpoint, + resolvedApiKey, + requestBody, + n, + targetSize, + handleUpstreamProgress, + ); + } catch (fetchError: unknown) { + if (fetchError instanceof DOMException && fetchError.name === 'AbortError') { + return NextResponse.json({ error: `自定义API请求超时(${GENERATION_TIMEOUT_SECONDS}秒)` }, { status: 504 }); + } + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('fetch failed')) { + return NextResponse.json({ error: `无法连接到自定义API: ${msg}。请检查 API 地址` }, { status: 502 }); + } + return NextResponse.json({ error: `自定义API网络错误: ${msg}` }, { status: 502 }); + } + + if (customGenerationResult.upstreamError) { + const { status, text } = customGenerationResult.upstreamError; + console.error('[Custom API Image Error]', status, text.slice(0, 500)); + return NextResponse.json( + { error: parseCustomApiError(status, text) }, + { status: status >= 500 ? 502 : status } + ); + } + + if (customGenerationResult.images.length === 0) { + return NextResponse.json({ error: lowResolutionError(targetSize, customGenerationResult.rejected) }, { status: 502 }); + } + console.log('[Custom API Image] Persisted', customGenerationResult.images.length, '/', n, 'qualified images', + '| target:', targetSize ? formatTargetSize(targetSize) : 'none'); + return NextResponse.json({ images: customGenerationResult.images }); + } catch (customError: unknown) { + const msg = customError instanceof Error ? customError.message : '自定义API请求异常'; + console.error('[Custom API Image Exception]', msg); + return NextResponse.json({ error: `自定义API异常: ${msg}` }, { status: 502 }); + } + } + + // ---- Default mode: use coze-coding-dev-sdk ---- + const customHeaders = HeaderUtils.extractForwardHeaders(request.headers); + const config = new Config(); + const client = new ImageGenerationClient(config, customHeaders); + + let sdkSize: string; + if (size) { + sdkSize = size; + } else if (aspectRatio && resolution) { + // Resolve from aspect ratio + resolution + const sizeMap: Record> = { + '1:1': { '1080P': '1024x1024', '2K': '2048x2048', '4K': '4096x4096' }, + '16:9': { '1080P': '1920x1080', '2K': '2560x1440', '4K': '3840x2160' }, + '9:16': { '1080P': '1080x1920', '2K': '1440x2560', '4K': '2160x3840' }, + '4:3': { '1080P': '1440x1080', '2K': '2560x1920', '4K': '4096x3072' }, + '3:4': { '1080P': '1080x1440', '2K': '1920x2560', '4K': '3072x4096' }, + }; + sdkSize = sizeMap[aspectRatio]?.[resolution] || '1024x1024'; + } else { + sdkSize = quality === '4K' ? '4K' : quality === '1K' ? '1K' : '2K'; + } + + const generateRequest: Record = { + prompt, + model, + size: sdkSize, + watermark: false, + }; + + if (negativePrompt) { + generateRequest.negativePrompt = negativePrompt; + } + + if (image) { + if (image.startsWith('data:')) { + const uploadedUrl = await uploadDataUrlAndGetPublicUrl(image); + if (uploadedUrl) { + generateRequest.image = uploadedUrl; + } else { + console.warn('[Image Gen] Failed to upload reference image, skipping'); + } + } else { + generateRequest.image = image; + } + } + + let response; + try { + const debugRequest = { ...generateRequest }; + if (typeof debugRequest.image === 'string' && debugRequest.image.length > 100) { + debugRequest.image = `${debugRequest.image.substring(0, 60)}... (${debugRequest.image.length} chars)`; + } + console.log('[SDK Image Request]', JSON.stringify(debugRequest)); + response = await client.generate(generateRequest as unknown as Parameters[0]); + } catch (sdkError: unknown) { + const sdkMessage = sdkError instanceof Error ? sdkError.message : '图片生成请求失败'; + let detail = ''; + try { + const errObj = sdkError as { response?: { status?: number; data?: unknown; statusText?: string } }; + if (errObj.response) { + const dataStr = errObj.response.data ? JSON.stringify(errObj.response.data) : ''; + detail = `status=${errObj.response.status} data=${dataStr.substring(0, 500)}`; + } + } catch { /* ignore */ } + console.error('[Image Generation SDK Error]', sdkMessage, detail); + if (image) { + return NextResponse.json({ + error: '图生图生成失败: 内置模型图生图功能暂不可用。建议使用自定义API重试。', + }, { status: 503 }); + } + return NextResponse.json({ error: `图片生成服务暂时不可用: ${sdkMessage}` }, { status: 503 }); + } + + const helper = client.getResponseHelper(response); + if (!helper.success) { + const errorMsg = helper.errorMessages.length > 0 ? helper.errorMessages.join('; ') : '图片生成失败'; + return NextResponse.json({ error: errorMsg }, { status: 500 }); + } + + const images = helper.imageUrls; + if (images.length === 0) { + return NextResponse.json({ error: '图片生成失败,请稍后重试' }, { status: 500 }); + } + + const persistedImages = await persistQualifiedImageUrls(images, 'generated/images', targetSize, 'SDK Image'); + if (persistedImages.images.length === 0) { + return NextResponse.json({ error: lowResolutionError(targetSize, persistedImages.rejected) }, { status: 502 }); + } + return NextResponse.json({ images: persistedImages.images }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '图片生成失败'; + console.error('[Image Generation Error]', message, error instanceof Error ? error.stack : ''); + return NextResponse.json({ error: `生成失败: ${message}` }, { status: 500 }); + } +} diff --git a/src/app/api/generate/reverse-prompt/route.ts b/src/app/api/generate/reverse-prompt/route.ts new file mode 100644 index 0000000..489f0bb --- /dev/null +++ b/src/app/api/generate/reverse-prompt/route.ts @@ -0,0 +1,262 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError } from '@/lib/custom-api-fetch'; +import { localStorage } from '@/lib/local-storage'; +import { resolveServerApiConfig } from '@/lib/server-api-config'; + +interface CustomApiConfig { + apiUrl: string; + modelName: string; + apiKey: string; + provider?: string; + customApiKeyId?: string; + systemApiId?: string; +} + +const REVERSE_PROMPT_TIMEOUT = 90_000; +const MAX_IMAGE_DATA_URL_LENGTH = 8_000_000; + +interface ReversePromptResult { + generalPrompt: string; + structuredPrompt: string; + negativePrompt: string; + structuredSections?: { + subject?: string; + environment?: string; + visualStyle?: string; + lighting?: string; + composition?: string; + character?: string; + }; +} + +function getDataUrlImage(image: string): { buffer: Buffer; contentType: string; extension: string } | null { + const match = image.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([\s\S]+)$/); + if (!match) return null; + const contentType = match[1].toLowerCase(); + const extensionMap: Record = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + 'image/gif': 'gif', + }; + const extension = extensionMap[contentType] || 'jpg'; + return { + buffer: Buffer.from(match[2], 'base64'), + contentType, + extension, + }; +} + +async function persistReferenceImage(image: string): Promise { + try { + if (image.startsWith('data:image/')) { + const parsed = getDataUrlImage(image); + if (!parsed) return null; + const key = `reverse-prompt/reference-images/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${parsed.extension}`; + const savedKey = await localStorage.uploadFile({ + fileContent: parsed.buffer, + fileName: key, + contentType: parsed.contentType, + }); + return localStorage.generatePresignedUrl({ key: savedKey, expireTime: 2592000 }); + } + + if (/^https?:\/\/\S+/i.test(image)) { + return localStorage.copyPublicUrlToFolder(image, 'reverse-prompt/reference-images'); + } + } catch (error) { + console.warn('[Reverse Prompt] persist reference image failed:', error); + } + return null; +} + +function parseReversePrompt(content: string): ReversePromptResult { + const trimmed = content.trim(); + const jsonMatch = trimmed.match(/\{[\s\S]*\}/); + if (jsonMatch) { + try { + const parsed = JSON.parse(jsonMatch[0]) as Record; + const generalPrompt = String(parsed.generalPrompt || parsed.general || parsed.prompt || '').trim(); + const structuredPrompt = String( + parsed.structuredPrompt || parsed.structured || parsed.fullPrompt || parsed.pixelPrompt || '', + ).trim(); + const negativePrompt = String(parsed.negativePrompt || parsed.negative || '').trim(); + const rawSections = parsed.structuredSections; + const structuredSections = rawSections && typeof rawSections === 'object' + ? { + subject: String((rawSections as Record).subject || '').trim() || undefined, + environment: String((rawSections as Record).environment || '').trim() || undefined, + visualStyle: String((rawSections as Record).visualStyle || (rawSections as Record).style || '').trim() || undefined, + lighting: String((rawSections as Record).lighting || '').trim() || undefined, + composition: String((rawSections as Record).composition || '').trim() || undefined, + character: String((rawSections as Record).character || (rawSections as Record).person || '').trim() || undefined, + } + : undefined; + if (generalPrompt || structuredPrompt) { + return { + generalPrompt: generalPrompt || structuredPrompt, + structuredPrompt: structuredPrompt || generalPrompt, + negativePrompt, + structuredSections, + }; + } + } catch { + // Fall through to plain text handling. + } + } + + return { + generalPrompt: trimmed, + structuredPrompt: trimmed, + negativePrompt: 'low quality, blurry, distorted anatomy, extra limbs, deformed hands, bad face, inaccurate details, text, watermark, logo, cropped subject, oversaturated, underexposed, overexposed', + }; +} + +function buildInstruction(outputMode: 'general' | 'structured' | 'pixel', language: 'zh' | 'en'): string { + const languageRule = language === 'en' + ? '所有提示词字段必须使用英文输出。' + : '所有提示词字段必须使用中文输出。'; + + if (outputMode === 'pixel') { + return `你是专业的图片反推提示词专家,同时熟悉 image2 / 图生图模型的提示词偏好。请严格观察用户上传的参考图,把图片转换为更适合 image2 参考图生成的高保真复刻提示词。目标不是普通描述,而是让 image2 在使用同一张参考图时尽可能保留人物身份、面部微表情、身高体态、肢体粗细、脸型、手脚细节、长相、身形、身材比例和服装场景。必须描述所有可见细节,不要编造看不见的内容。 + +输出必须只返回 JSON,不要解释,不要 Markdown。JSON 格式只允许包含这两个字段: +{ + "structuredPrompt": "完整提示词。必须是一段可直接粘贴到 image2 正向提示词输入框的复刻型提示词,先写参考图硬约束和保真目标,再写人物身份锚点、面部微表情锚点、身高体态和身体比例锚点、手脚肢体锚点、服装材质锚点、构图光影色彩锚点,最后按画面区域逐块补全细节", + "negativePrompt": "反向提示词。必须列出会破坏参考图一致性的错误,包括不同人物、不同脸型、错误表情、错误身材比例、手脚畸形、肢体粗细变化、胸腰臀比例变化、过度美化、重设计服装、改构图和低质量问题" +} + +image2 复刻级要求: +1. 只输出完整提示词和反向提示词,不要输出通用描述、结构化分项、解释文字或 Markdown。 +2. 完整提示词第一句必须明确:以参考图为硬视觉参考,保留同一个人物/主体,不重新设计,不随机换脸,不改变身材比例,不做额外美化。 +3. 完整提示词必须按 image2 更容易执行的顺序组织:保真目标 -> 主体身份和年龄气质 -> 面部骨相和五官比例 -> 面部微表情和眼神 -> 头发和皮肤纹理 -> 身高体态和身体比例 -> 手部脚部及四肢 -> 服装配饰 -> 构图镜头 -> 光影色彩 -> 背景道具 -> 画面瑕疵纹理。 +4. 人像必须写成“身份锁定”而不是普通外貌描述:脸型轮廓、额头、颧骨、下颌线、下巴、脸宽脸长比例、眉眼间距、眼型、眼睑开合、瞳孔/视线方向、鼻梁鼻尖鼻翼、嘴唇厚薄、嘴部开合、嘴角方向、法令纹/酒窝/痣/斑点/毛孔/皮肤质感、左右不对称特征、真实年龄感和气质都要尽量描述。 +5. 面部微表情必须具体到可见肌肉和局部状态:眉毛高低、眼周紧张或放松、眼神情绪、脸颊受力、嘴角上扬/下压幅度、唇线、牙齿是否可见、下巴和颈部状态;不要只写“微笑”“严肃”等泛词。 +6. 身高、身形、身材和肢体必须用相对比例锁定:人物在画面中占比、头身比、肩宽相对脸宽、颈长、胸廓/腰/髋的可见轮廓、成人非情色语境下的胸部体积和服装包裹形态、手臂粗细、手腕、手掌大小、手指长度和弯曲、腿长、膝盖、小腿脚踝粗细、脚部大小和朝向;不可把身材重塑成更瘦、更高、更丰满或更夸张。 +7. 手部和脚部要单独描述:可见手指数量、手指姿态、关节弯曲、指尖方向、手掌遮挡关系、脚趾/鞋型/脚背/脚踝可见状态;要求保持自然解剖结构,避免多指、少指、粘连、变形和错误遮挡。 +8. 服装、配饰和材质必须写清楚款式、剪裁、贴身/宽松程度、领口袖口下摆、布料厚薄、褶皱走向、拉伸变形、透明度、反光、花纹、缝线、饰品位置和遮挡关系;不要让 image2 自行换装或增强性感化。 +9. 构图必须锁定画面比例、景别、视角、镜头高度、焦段感、主体在画面中的位置、裁切边界、头顶/脚底/四肢与画面边缘的距离、前景中景背景层次、透视和景深。 +10. 颜色和光影必须描述主色、辅色、肤色倾向、衣物色块、背景色块、色温、光源方向、软硬、明暗边界、高光、阴影、反射、环境光、颗粒、压缩痕迹、噪点、模糊和瑕疵。 +11. 按画面区域补充细节时,可以用九宫格、前景/中景/背景、或主体局部区域划分;每个区域都要写清楚位置、可见物体、大小比例、边缘形状、材质纹理、遮挡关系和小瑕疵。 +12. 如果有文字、Logo、图标、符号、品牌标识或界面元素,必须描述可识别的内容、字体观感、颜色、大小、排列方式和具体位置;不可完全识别时只描述可见形态,不要臆造。 +13. negativePrompt 必须优先排除破坏参考图相似度的内容:different person, changed identity, wrong face shape, different expression, changed gaze, altered body proportions, different height impression, thinner arms, thicker legs, changed bust/waist/hip proportion, deformed hands, wrong fingers, deformed feet, over-beautified face, plastic skin, redesigned outfit, different pose, different camera angle, different crop, extra objects, missing details。 +14. 不要写“图片中”“这张图”等元描述,直接写可用于生成模型的提示词。 +15. ${languageRule}`; + } + + const preferred = outputMode === 'structured' ? '结构化提示词' : '通用描述提示词'; + + return `你是专业的图片反推提示词专家。请严格观察用户上传的参考图,把图片转换为可直接用于 AI 文生图模型的提示词,目标是让用户把提示词交给文生图模型后尽可能还原原图。必须描述所有可见细节,不要编造看不见的内容。 + +输出必须只返回 JSON,不要解释,不要 Markdown。JSON 格式: +{ + "generalPrompt": "通用描述提示词,使用连贯自然语言完整描述主体、环境、画面、风格、光照、构图、色彩、材质、镜头感和所有关键细节", + "structuredPrompt": "结构化提示词,分段包含:主题、环境、视觉风格、光照、构图;如果有人物,还必须包含人物身材比例、面部细节、面部微表情、嘴部和嘴角细节、眼神细节、发型、配饰、衣物、衣物质感、姿态、身体朝向、画面比例等", + "structuredSections": { + "subject": "主题/主体,描述主体身份、数量、动作、核心物体、关键视觉特征,以及主体与画面其他元素的关系", + "environment": "环境,描述空间、背景、道具、天气、时代、场景关系、前景/中景/远景元素", + "visualStyle": "视觉风格,描述画风、质感、色彩、镜头语言、渲染/摄影特征、清晰度、颗粒感、景深和后期效果", + "lighting": "光照,描述光源方向、软硬、色温、明暗关系、反射、高光、阴影、轮廓光和环境光", + "composition": "构图,描述景别、视角、主体位置、画面比例、裁切、留白、透视、镜头焦段感和画面重心", + "character": "如果有人物,描述身材比例、体态、肩颈腰腿比例、脸型、肤色、眉眼鼻唇、面部微表情、嘴部形态、嘴角方向、眼神方向和情绪、发型、头饰、配饰、衣物款式、衣物材质、褶皱、透明度、姿态、手部细节和身体朝向;无人物则为空字符串" + }, + "negativePrompt": "反向提示词,列出需要避免的低质量、错误结构、畸形、模糊、文字水印、不符合原图的元素" +} + +细节要求: +1. ${preferred}要更适合当前选择的输出形式,但 generalPrompt 和 structuredPrompt 两个字段都必须生成。 +2. 人物图片必须尽可能细致描述人物整体每一个可见细节:身材比例、体态、脸型、肤质、五官比例、眉毛、眼睛形状、瞳孔方向、眼神情绪、鼻梁、嘴唇厚薄、嘴部开合、嘴角上扬或下压、面部微表情、发型、发丝状态、配饰、服装款式、衣物材质、纹理、褶皱、透明度、姿态、手指、肢体动作和身体朝向。 +3. 如果参考图包含文字、Logo、图标、符号、品牌标识或界面元素,必须仔细描述可识别的文字内容、字体观感、颜色、大小、排列方式,以及它们在图片中的具体位置;如果文字不可完全识别,要说明可见形态,不要臆造。 +4. 产品、建筑、场景图片必须具体描述形状、材质、颜色、空间关系、背景、光照、反射、纹理、磨损、边缘轮廓和比例关系。 +5. 需要描述画面中的小物件、局部装饰、材质反光、阴影、高光、背景细节和遮挡关系,避免只写大概风格。 +6. 不要写“图片中”“这张图”等元描述,直接写可用于生成模型的提示词。 +7. ${languageRule}`; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const image = typeof body.image === 'string' ? body.image : ''; + const outputMode = body.outputMode === 'general' + ? 'general' + : body.outputMode === 'pixel' + ? 'pixel' + : 'structured'; + const language = body.language === 'en' ? 'en' : 'zh'; + const customApiConfig = body.customApiConfig as CustomApiConfig | undefined; + + const isDataImage = image.startsWith('data:image/'); + const isHttpImage = /^https?:\/\/\S+/i.test(image); + if (!image || (!isDataImage && !isHttpImage)) { + return NextResponse.json({ error: '请上传需要反推提示词的图片' }, { status: 400 }); + } + if (isDataImage && image.length > MAX_IMAGE_DATA_URL_LENGTH) { + return NextResponse.json({ error: '图片过大,请压缩后再上传' }, { status: 400 }); + } + const resolvedCustomApiConfig = await resolveServerApiConfig(request, customApiConfig); + if (!resolvedCustomApiConfig?.apiKey || !resolvedCustomApiConfig.apiUrl || !resolvedCustomApiConfig.modelName) { + return NextResponse.json({ error: '未配置可用的多模态模型,请先在 API 设置中添加支持图片理解的多模态模型' }, { status: 400 }); + } + const resolvedApiKey = resolvedCustomApiConfig.apiKey; + const persistedReferenceImage = await persistReferenceImage(image); + + const chatBody = { + model: resolvedCustomApiConfig.modelName, + stream: false, + messages: [ + { role: 'system', content: buildInstruction(outputMode, language) }, + { + role: 'user', + content: [ + { + type: 'text', + text: '请根据这张参考图反推出文生图提示词,尽可能完整还原画面细节,并严格按 JSON 格式返回。', + }, + { + type: 'image_url', + image_url: { url: image }, + }, + ], + }, + ], + }; + + const response = await fetchWithRetry( + resolvedCustomApiConfig.apiUrl, + { + method: 'POST', + headers: buildCustomApiHeaders(resolvedApiKey), + body: JSON.stringify(chatBody), + }, + REVERSE_PROMPT_TIMEOUT, + 1, + ); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: parseCustomApiError(response.status, errorText) }, + { status: response.status >= 500 ? 502 : response.status }, + ); + } + + const data = await response.json(); + const choices = (data as Record).choices as Array> | undefined; + const message = choices?.[0]?.message as Record | undefined; + const content = message?.content; + + if (typeof content !== 'string' || !content.trim()) { + return NextResponse.json({ error: '模型未返回有效的反推提示词' }, { status: 502 }); + } + + return NextResponse.json({ + ...parseReversePrompt(content), + referenceImage: persistedReferenceImage, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '图片反推提示词失败'; + console.error('[Reverse Prompt Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/generate/suggest-prompt/route.ts b/src/app/api/generate/suggest-prompt/route.ts new file mode 100644 index 0000000..e5aa79a --- /dev/null +++ b/src/app/api/generate/suggest-prompt/route.ts @@ -0,0 +1,151 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError } from '@/lib/custom-api-fetch'; +import { resolveServerApiConfig } from '@/lib/server-api-config'; + +interface CustomApiConfig { + apiUrl: string; + modelName: string; + apiKey: string; + provider: string; + customApiKeyId?: string; + systemApiId?: string; +} + +const SUGGEST_TIMEOUT = 60_000; + +function parseOptimizedPrompt(content: string): { prompt: string; negativePrompt?: string } { + const trimmed = content.trim(); + const jsonMatch = trimmed.match(/\{[\s\S]*\}/); + if (jsonMatch) { + try { + const parsed = JSON.parse(jsonMatch[0]) as Record; + const prompt = typeof parsed.prompt === 'string' + ? parsed.prompt + : typeof parsed.positivePrompt === 'string' + ? parsed.positivePrompt + : typeof parsed.positive === 'string' + ? parsed.positive + : ''; + const negativePrompt = typeof parsed.negativePrompt === 'string' + ? parsed.negativePrompt + : typeof parsed.negative === 'string' + ? parsed.negative + : ''; + if (prompt.trim()) { + return { + prompt: prompt.trim(), + negativePrompt: negativePrompt.trim() || undefined, + }; + } + } catch { + // Fall through to labeled text parsing. + } + } + + const positiveMatch = trimmed.match(/(?:正向提示词|优化提示词|正面提示词|Positive Prompt|Prompt)\s*[::]\s*([\s\S]*?)(?=(?:负向提示词|负面提示词|反向提示词|Negative Prompt|Negative)\s*[::]|$)/i); + const negativeMatch = trimmed.match(/(?:负向提示词|负面提示词|反向提示词|Negative Prompt|Negative)\s*[::]\s*([\s\S]*)$/i); + const prompt = (positiveMatch?.[1] || trimmed).trim(); + const negativePrompt = negativeMatch?.[1]?.trim(); + + return { + prompt, + negativePrompt: negativePrompt || undefined, + }; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + prompt, + customApiConfig, + systemPrefix, + } = body as { + prompt?: string; + modelName?: string; + customApiConfig?: CustomApiConfig; + systemPrefix?: string; + }; + + if (!prompt) { + return NextResponse.json({ error: '请提供创作描述' }, { status: 400 }); + } + + const resolvedCustomApiConfig = await resolveServerApiConfig(request, customApiConfig); + + // Use custom/system multimodal model API if provided + if (resolvedCustomApiConfig && resolvedCustomApiConfig.apiKey) { + const resolvedApiKey = resolvedCustomApiConfig.apiKey; + const endpoint = resolvedCustomApiConfig.apiUrl; + if (!endpoint) { + return NextResponse.json({ error: '多模态模型API未配置请求地址' }, { status: 400 }); + } + if (!resolvedCustomApiConfig.modelName) { + return NextResponse.json({ error: '多模态模型API未配置模型名称' }, { status: 400 }); + } + + // Build system message with optional prefix + const baseInstruction = systemPrefix + ? `${systemPrefix}。` + : '你是一个专业的AI绘图/视频提示词优化专家。'; + const systemMessage = `${baseInstruction}请基于用户描述同时生成正向提示词和反向/负面提示词。正向提示词要更详细、更有画面感、更适合生成模型;负面提示词要列出应避免的低质量、畸形、错误结构、画面瑕疵、文字水印等内容。只返回JSON,不要解释,格式为:{"prompt":"优化后的正向提示词","negativePrompt":"优化后的负面提示词"}`; + + const headers = buildCustomApiHeaders(resolvedApiKey); + const chatBody = { + model: resolvedCustomApiConfig.modelName, + stream: false, + messages: [ + { role: 'system', content: systemMessage }, + { role: 'user', content: prompt }, + ], + }; + + console.log('[Suggest Prompt] Using custom multimodal model:', resolvedCustomApiConfig.modelName, '| prefix:', systemPrefix || 'default'); + + try { + const response = await fetchWithRetry( + endpoint, + { + method: 'POST', + headers, + body: JSON.stringify(chatBody), + }, + SUGGEST_TIMEOUT, + 1, + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Suggest Prompt] API error:', response.status, errorText.slice(0, 200)); + return NextResponse.json( + { error: parseCustomApiError(response.status, errorText) }, + { status: response.status >= 500 ? 502 : response.status } + ); + } + + const data = await response.json(); + const choices = (data as Record).choices as Array> | undefined; + if (choices && choices.length > 0) { + const message = choices[0].message as Record; + const content = message?.content; + if (typeof content === 'string' && content.trim()) { + return NextResponse.json(parseOptimizedPrompt(content)); + } + } + + return NextResponse.json({ error: '多模态模型未返回有效内容' }, { status: 502 }); + } catch (fetchError: unknown) { + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + console.error('[Suggest Prompt] Fetch error:', msg); + return NextResponse.json({ error: `提示词优化失败: ${msg}` }, { status: 502 }); + } + } + + // No multimodal model configured + return NextResponse.json({ error: '未配置多模态模型,请在API设置中添加多模态模型' }, { status: 400 }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '提示词优化失败'; + console.error('[Suggest Prompt Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/generate/video/route.ts b/src/app/api/generate/video/route.ts new file mode 100644 index 0000000..0be4c78 --- /dev/null +++ b/src/app/api/generate/video/route.ts @@ -0,0 +1,651 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { VideoGenerationClient, Config, HeaderUtils } from 'coze-coding-dev-sdk'; +import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError, parseCustomApiJsonWithProgress } from '@/lib/custom-api-fetch'; +import { getAspectRatioPromptHint } from '@/lib/model-config'; +import { localStorage } from '@/lib/local-storage'; +import { isTrustedInternalGenerationRequest, isUuid, resolveServerApiConfig } from '@/lib/server-api-config'; +import { updateGenerationJobProgress } from '@/lib/generation-job-estimates'; +import { + compressImageBufferForUpstream, + dataUrlToImageBuffer, + imageBufferToDataUrl, +} from '@/lib/server-image-compression'; + +interface CustomApiConfig { + apiUrl: string; + modelName: string; + apiKey: string; + provider: string; + customApiKeyId?: string; + systemApiId?: string; +} + +const GENERATION_TIMEOUT = 180_000; +const MAX_UPSTREAM_REFERENCE_IMAGE_BYTES = Number(process.env.MAX_UPSTREAM_REFERENCE_IMAGE_BYTES || 700 * 1024); + +export const runtime = 'nodejs'; + +/** + * Upload a media data URL to S3 storage and return a presigned URL. + * Includes a 45s timeout to prevent blocking the response. + */ +async function persistMediaToStorage(dataUrl: string, prefix: string): Promise { + if (!dataUrl.startsWith('data:')) return dataUrl; + + try { + const match = dataUrl.match(/^data:((?:image|video)\/[^;]+);base64,(.+)$/); + if (!match) return dataUrl; + const [, mimeType, base64Data] = match; + const ext = mimeType.split('/')[1] || 'mp4'; + const buffer = Buffer.from(base64Data, 'base64'); + const fileName = `${prefix}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + + const fileKey = await withTimeout( + localStorage.uploadFile({ fileContent: buffer, fileName, contentType: mimeType }), + 45_000, + 'Local uploadFile (video)', + ); + + if (!fileKey) { + console.error('[Persist Video Media] uploadFile returned empty key'); + return dataUrl; + } + + const presignedUrl = await withTimeout( + localStorage.generatePresignedUrl({ key: fileKey, expireTime: 2592000 }), + 10_000, + 'Local generatePresignedUrl (video)', + ); + + if (presignedUrl) { + console.log('[Persist Video Media] Success, key:', fileKey, 'size:', buffer.length, 'bytes'); + return presignedUrl; + } + + return dataUrl; + } catch (err) { + console.error('[Persist Video Media Error]', err instanceof Error ? err.message : err); + return dataUrl; + } +} + +async function persistRemoteUrlToStorage(url: string, prefix: string): Promise { + if (!url.startsWith('http')) return url; + + try { + const fileKey = await withTimeout( + localStorage.uploadFromUrl({ url, timeout: 60000 }), + 60_000, + 'Local uploadFromUrl (video)', + ); + if (!fileKey) return url; + + const presignedUrl = await withTimeout( + localStorage.generatePresignedUrl({ key: fileKey, expireTime: 2592000 }), + 10_000, + 'Local generatePresignedUrl (video remote)', + ); + + if (presignedUrl) { + console.log('[Persist Remote Video URL] Success, key:', fileKey); + return presignedUrl; + } + return url; + } catch (err) { + console.warn('[Persist Remote Video URL] Failed, using original URL:', err instanceof Error ? err.message : err); + return url; + } +} + +/** Helper: wrap a promise with a timeout that rejects with a descriptive message */ +function withTimeout(promise: Promise, ms: number, label: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + promise.then( + (v) => { clearTimeout(timer); resolve(v); }, + (e) => { clearTimeout(timer); reject(e); }, + ); + }); +} + +async function persistAllMediaUrls(urls: string[], prefix: string): Promise { + const MAX_DATA_URL_SIZE = 10 * 1024 * 1024; // 10MB limit for video data URLs + const results = await Promise.all( + urls.map(async (url) => { + try { + if (url.startsWith('data:')) { + const result = await persistMediaToStorage(url, prefix); + if (result.startsWith('data:') && result.length > MAX_DATA_URL_SIZE) { + console.warn('[Persist Video] Data URL too large (' + Math.round(result.length / 1024 / 1024) + 'MB), skipping'); + return null; + } + return result; + } + if (url.startsWith('http')) return persistRemoteUrlToStorage(url, prefix); + return url; + } catch (err) { + console.error('[persistAllMediaUrls video] Error:', err instanceof Error ? err.message : err); + if (url.startsWith('data:') && url.length > MAX_DATA_URL_SIZE) return null; + return url; + } + }), + ); + return results.filter((u): u is string => u !== null); +} + +async function uploadDataUrlAndGetPublicUrl(dataUrl: string): Promise { + try { + const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/); + if (!match) return null; + const [, mimeType, base64Data] = match; + const ext = mimeType.split('/')[1] || 'png'; + const buffer = Buffer.from(base64Data, 'base64'); + const fileName = `img2vid-ref/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + + const fileKey = await localStorage.uploadFile({ fileContent: buffer, fileName, contentType: mimeType }); + if (!fileKey) return null; + + const presignedUrl = await localStorage.generatePresignedUrl({ key: fileKey, expireTime: 3600 }); + console.log('[Upload Ref Video Image] Success, key:', fileKey); + return presignedUrl || null; + } catch (err) { + console.error('[Upload Ref Video Image Error]', err instanceof Error ? err.message : err); + return null; + } +} + +async function toPublicImageUrl(image: string): Promise { + if (!image.startsWith('data:')) return image; + const uploadedUrl = await uploadDataUrlAndGetPublicUrl(image); + return uploadedUrl || image; +} + +async function normalizeReferenceImageForUpstream(image: string): Promise { + const parsedImage = dataUrlToImageBuffer(image); + if (!parsedImage) return image; + + try { + const compressed = await compressImageBufferForUpstream(parsedImage, { + maxBytes: MAX_UPSTREAM_REFERENCE_IMAGE_BYTES, + }); + if (compressed.changed) { + console.log('[Custom API img2vid] Compressed reference image:', compressed.originalBytes, '→', compressed.buffer.length); + } + return imageBufferToDataUrl({ buffer: compressed.buffer, mimeType: compressed.mimeType }); + } catch (err) { + console.warn('[Custom API img2vid] Reference image compression failed, using original:', err instanceof Error ? err.message : err); + return image; + } +} + +function normalizeReferenceImages(image?: string, images?: unknown, extraImages?: unknown): string[] { + const refs: string[] = []; + if (image) refs.push(image); + if (Array.isArray(images)) { + for (const item of images) { + if (typeof item === 'string' && item.trim()) refs.push(item); + } + } + if (Array.isArray(extraImages)) { + for (const item of extraImages) { + if (typeof item === 'string' && item.trim()) refs.push(item); + } + } + return Array.from(new Set(refs)); +} + +function uniqueStrings(values: string[]): string[] { + return Array.from(new Set(values)); +} + +function deriveChatCompletionsUrl(originalUrl: string): string { + if (originalUrl.includes('/chat/completions')) return originalUrl; + return originalUrl + .replace(/\/(videos|images)\/(generations|edits).*/i, '/chat/completions') + .replace(/\/+$/, ''); +} + +function deriveImagesEditsUrl(originalUrl: string): string { + if (originalUrl.includes('/images/edits')) return originalUrl; + return originalUrl + .replace(/\/(videos|images)\/generations.*/i, '/images/edits') + .replace(/\/+$/, ''); +} + +function extractVideosFromChatResponse(data: Record): string[] { + const videos: string[] = []; + const choices = data.choices as Array> | undefined; + if (Array.isArray(choices)) { + for (const choice of choices) { + const message = choice.message as Record | undefined; + if (!message) continue; + const content = message.content; + if (typeof content === 'string') { + if (content.startsWith('http') || content.startsWith('data:video/')) videos.push(content); + const urlMatch = content.match(/(https?:\/\/[^\s"']+\.(mp4|mov|webm)[^\s"']*)/i); + if (urlMatch) videos.push(urlMatch[1]); + } else if (Array.isArray(content)) { + for (const item of content as Array>) { + if (item.type === 'video_url' && item.video_url) { + const url = (item.video_url as Record).url; + if (typeof url === 'string') videos.push(url); + } + if (item.type === 'text' && typeof item.text === 'string') { + const text = item.text as string; + if (text.startsWith('http') || text.startsWith('data:video/')) videos.push(text); + const urlMatch = text.match(/(https?:\/\/[^\s"']+\.(mp4|mov|webm)[^\s"']*)/i); + if (urlMatch) videos.push(urlMatch[1]); + } + } + } + } + } + return videos; +} + +function extractVideosFromGenerationsResponse(data: Record): string[] { + const videos: string[] = []; + if (Array.isArray(data.data)) { + for (const item of data.data as Array>) { + if (typeof item === 'string') { videos.push(item); continue; } + if (item.url && typeof item.url === 'string') videos.push(item.url); + if (item.video_url && typeof item.video_url === 'string') videos.push(item.video_url); + if (item.b64_json && typeof item.b64_json === 'string') { + videos.push(`data:video/mp4;base64,${item.b64_json}`); + } + } + } else if (typeof data.url === 'string') { + videos.push(data.url); + } else if (typeof data.video_url === 'string') { + videos.push(data.video_url); + } + return videos; +} + +async function customApiImageToVideo( + customApiConfig: CustomApiConfig, + prompt: string | undefined, + negativePrompt: string | undefined, + image: string, + referenceImages: string[] = [], + aspectRatio?: string, + duration?: number, + fps?: number, + onProgress?: (progress: Record) => void | Promise, +): Promise { + const endpoint = customApiConfig.apiUrl; + if (!endpoint) { + return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 }); + } + if (!customApiConfig.modelName) { + return NextResponse.json({ error: '自定义API未配置模型名称' }, { status: 400 }); + } + + const normalizedImage = await normalizeReferenceImageForUpstream(image); + const normalizedReferenceImages = uniqueStrings(await Promise.all( + normalizeReferenceImages(normalizedImage, referenceImages).map(normalizeReferenceImageForUpstream), + )); + + // Prepare image buffer for FormData upload + let imageBuffer: Buffer | null = null; + let imageMimeType = 'image/png'; + if (normalizedImage.startsWith('data:')) { + const parsedImage = dataUrlToImageBuffer(normalizedImage); + if (parsedImage) { + imageMimeType = parsedImage.mimeType; + imageBuffer = parsedImage.buffer; + } + } + + // Upload reference image to S3 + const imageUrl = await toPublicImageUrl(normalizedImage); + const imageUrls = await Promise.all(normalizedReferenceImages.map(toPublicImageUrl)); + + let promptText = prompt || '根据参考图生成视频'; + if (negativePrompt) promptText += `\n\n负面提示词: ${negativePrompt}`; + // Augment prompt with aspect ratio hint + if (aspectRatio) { + const hint = getAspectRatioPromptHint(aspectRatio); + if (hint) promptText += `\n\n[${hint}]`; + } + + const headers = buildCustomApiHeaders(customApiConfig.apiKey); + + // Get raw base64 for strategies that need it + let rawBase64 = normalizedImage; + if (normalizedImage.startsWith('data:')) { + const commaIndex = normalizedImage.indexOf(','); + if (commaIndex !== -1) rawBase64 = normalizedImage.substring(commaIndex + 1); + } + + const strategyResults: string[] = []; + let firstUpstreamError: { error: string; status: number } | null = null; + + // --- Strategy 1: images/edits with multipart/form-data --- + // Same as img2img - Cherry Studio uses multipart/form-data for image-based requests + if (imageBuffer) { + const editsUrl = deriveImagesEditsUrl(endpoint); + console.log('[Custom API img2vid → 策略1: images/edits (FormData)] URL:', editsUrl, '| model:', customApiConfig.modelName); + try { + const boundary = `----FormBoundary${Date.now()}${Math.random().toString(36).slice(2)}`; + const parts: Buffer[] = []; + + const textFields: Record = { + model: customApiConfig.modelName, + prompt: promptText, + }; + if (aspectRatio) textFields.aspect_ratio = aspectRatio; + if (duration) textFields.duration = String(duration); + if (fps) textFields.fps = String(fps); + + for (const [key, value] of Object.entries(textFields)) { + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n` + )); + } + + const ext = imageMimeType.split('/')[1] || 'png'; + const imageBuffers: Array<{ mimeType: string; buffer: Buffer }> = []; + for (const ref of normalizedReferenceImages) { + if (!ref.startsWith('data:')) continue; + const parsedImage = dataUrlToImageBuffer(ref); + if (!parsedImage) continue; + imageBuffers.push(parsedImage); + } + + imageBuffers.forEach((item, index) => { + const fieldName = index === 0 ? 'image' : 'images[]'; + const itemExt = item.mimeType.split('/')[1] || ext; + parts.push(Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="image-${index + 1}.${itemExt}"\r\nContent-Type: ${item.mimeType}\r\n\r\n` + )); + parts.push(item.buffer); + parts.push(Buffer.from(`\r\n`)); + }); + parts.push(Buffer.from(`--${boundary}--\r\n`)); + + const bodyBuffer = Buffer.concat(parts); + + const editsResponse = await fetchWithRetry( + editsUrl, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${customApiConfig.apiKey}`, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + }, + body: bodyBuffer, + }, + GENERATION_TIMEOUT, + 1, + ); + if (editsResponse.ok) { + const editsData = await parseCustomApiJsonWithProgress(editsResponse, onProgress); + let videos = extractVideosFromGenerationsResponse(editsData as Record); + if (videos.length === 0) videos = extractVideosFromChatResponse(editsData as Record); + if (videos.length > 0) { + const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos'); + return NextResponse.json({ videos: persistedVideos }); + } + strategyResults.push('策略1(images/edits FormData): 响应中无视频数据'); + } else { + const errorText = await editsResponse.text(); + const parsedError = parseCustomApiError(editsResponse.status, errorText); + if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: editsResponse.status }; + strategyResults.push(parsedError); + } + } catch (err) { + strategyResults.push(`策略1(images/edits FormData): ${err instanceof Error ? err.message : '异常'}`); + } + } + + // --- Strategy 2: chat/completions with image_url --- + const chatUrl = deriveChatCompletionsUrl(endpoint); + const chatBody: Record = { + model: customApiConfig.modelName, + stream: false, + messages: [ + { + role: 'user', + content: [ + ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } })), + { type: 'text', text: promptText }, + ], + }, + ], + }; + if (aspectRatio) chatBody.aspect_ratio = aspectRatio; + if (duration) chatBody.duration = duration; + if (fps) chatBody.fps = fps; + + console.log('[Custom API img2vid → 策略2: chat/completions] URL:', chatUrl, '| model:', customApiConfig.modelName); + try { + const chatResponse = await fetchWithRetry(chatUrl, { method: 'POST', headers, body: JSON.stringify(chatBody) }, GENERATION_TIMEOUT, 1); + if (chatResponse.ok) { + const chatData = await parseCustomApiJsonWithProgress(chatResponse, onProgress); + let videos = extractVideosFromChatResponse(chatData as Record); + if (videos.length === 0) videos = extractVideosFromGenerationsResponse(chatData as Record); + if (videos.length > 0) { + const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos'); + return NextResponse.json({ videos: persistedVideos }); + } + } else { + const errorText = await chatResponse.text(); + const parsedError = parseCustomApiError(chatResponse.status, errorText); + if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: chatResponse.status }; + strategyResults.push(parsedError); + } + } catch (err) { + strategyResults.push(`策略2(chat/completions): ${err instanceof Error ? err.message : '异常'}`); + } + + // --- Strategy 3: images/generations with init_image --- + const imgBody: Record = { + model: customApiConfig.modelName, + prompt: promptText, + n: 1, + size: '1024x1024', + response_format: 'b64_json', + init_image: rawBase64, + images: imageUrls, + }; + if (aspectRatio) imgBody.aspect_ratio = aspectRatio; + if (duration) imgBody.duration = duration; + if (fps) imgBody.fps = fps; + + console.log('[Custom API img2vid → 策略3: images/generations] URL:', endpoint, '| model:', customApiConfig.modelName); + try { + const imgResponse = await fetchWithRetry(endpoint, { method: 'POST', headers, body: JSON.stringify(imgBody) }, GENERATION_TIMEOUT, 1); + if (!imgResponse.ok) { + const errorText = await imgResponse.text(); + const parsedError = parseCustomApiError(imgResponse.status, errorText); + if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: imgResponse.status }; + strategyResults.push(parsedError); + } else { + const imgData = await parseCustomApiJsonWithProgress(imgResponse, onProgress); + const videos = extractVideosFromGenerationsResponse(imgData as Record); + if (videos.length > 0) { + const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos'); + return NextResponse.json({ videos: persistedVideos }); + } + strategyResults.push('策略3(images/generations): 响应中无视频数据'); + } + } catch (err) { + strategyResults.push(`策略3(images/generations): ${err instanceof Error ? err.message : '异常'}`); + } + + return NextResponse.json( + { + error: firstUpstreamError?.error || strategyResults.find(Boolean) || '图生视频失败', + }, + { status: firstUpstreamError && firstUpstreamError.status < 500 ? firstUpstreamError.status : 502 } + ); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + prompt, + negativePrompt, + model = 'doubao-seedance-1-5-pro-251215', + aspectRatio = '16:9', + duration = 5, + fps = 30, + image, + images, + extraImages, + customApiConfig, + } = body as { + prompt?: string; + negativePrompt?: string; + model?: string; + aspectRatio?: string; + duration?: number; + fps?: number; + image?: string; + images?: string[]; + extraImages?: string[]; + customApiConfig?: CustomApiConfig; + }; + const referenceImages = normalizeReferenceImages(image, images, extraImages); + + if (!prompt && referenceImages.length === 0) { + return NextResponse.json({ error: '请提供视频描述或上传图片' }, { status: 400 }); + } + const trustedInternalRequest = isTrustedInternalGenerationRequest(request); + const trustedUserId = trustedInternalRequest + ? request.headers.get('x-miaojing-generation-user-id') + : null; + const generationJobId = trustedInternalRequest + ? request.headers.get('x-miaojing-generation-job-id') + : null; + const resolvedCustomApiConfig = await resolveServerApiConfig( + request, + customApiConfig, + isUuid(trustedUserId) ? trustedUserId : null, + ); + const handleUpstreamProgress = (progress: Record) => updateGenerationJobProgress( + isUuid(generationJobId) ? generationJobId : null, + progress, + ); + + // ---- Custom API mode ---- + if (resolvedCustomApiConfig && resolvedCustomApiConfig.apiKey) { + const resolvedApiKey = resolvedCustomApiConfig.apiKey; + try { + if (referenceImages.length > 0) { + return await customApiImageToVideo( + resolvedCustomApiConfig as CustomApiConfig, + prompt, + negativePrompt, + referenceImages[0], + referenceImages, + aspectRatio, + duration, + fps, + handleUpstreamProgress, + ); + } + + // Text-to-video + const endpoint = resolvedCustomApiConfig.apiUrl; + if (!endpoint) return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 }); + if (!resolvedCustomApiConfig.modelName) return NextResponse.json({ error: '自定义API未配置模型名称' }, { status: 400 }); + + // Augment prompt with aspect ratio hint as fallback + const ratioHint = aspectRatio ? getAspectRatioPromptHint(aspectRatio) : ''; + const augmentedPrompt = ratioHint ? `${prompt || ''}\n\n[${ratioHint}]` : (prompt || ''); + + const requestBody: Record = { + model: resolvedCustomApiConfig.modelName, + prompt: augmentedPrompt, + n: 1, + size: '1024x1024', + response_format: 'b64_json', + }; + if (negativePrompt) requestBody.negative_prompt = negativePrompt; + // Pass creation parameters for APIs that support them + if (aspectRatio) requestBody.aspect_ratio = aspectRatio; + if (duration) requestBody.duration = duration; + if (fps) requestBody.fps = fps; + + console.log('[Custom API Video] Text-to-video, sending to:', endpoint, '| model:', requestBody.model); + + let customResponse: Response; + try { + customResponse = await fetchWithRetry( + endpoint, + { method: 'POST', headers: buildCustomApiHeaders(resolvedApiKey), body: JSON.stringify(requestBody) }, + GENERATION_TIMEOUT, 1, + ); + } catch (fetchError: unknown) { + if (fetchError instanceof DOMException && fetchError.name === 'AbortError') { + return NextResponse.json({ error: '自定义API请求超时(180秒)' }, { status: 504 }); + } + const msg = fetchError instanceof Error ? fetchError.message : '请求失败'; + return NextResponse.json({ error: `自定义API网络错误: ${msg}` }, { status: 502 }); + } + + if (!customResponse.ok) { + const errorText = await customResponse.text(); + return NextResponse.json( + { error: parseCustomApiError(customResponse.status, errorText) }, + { status: customResponse.status >= 500 ? 502 : customResponse.status } + ); + } + + const customData = await parseCustomApiJsonWithProgress(customResponse, handleUpstreamProgress); + const videos = extractVideosFromGenerationsResponse(customData as Record); + if (videos.length === 0) { + return NextResponse.json({ error: '自定义API未返回有效视频数据', raw: customData }, { status: 502 }); + } + // Persist all data URLs and remote URLs to S3 + const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos'); + return NextResponse.json({ videos: persistedVideos }); + } catch (customError: unknown) { + const msg = customError instanceof Error ? customError.message : '自定义API请求异常'; + console.error('[Custom API Video Exception]', msg); + return NextResponse.json({ error: `自定义API异常: ${msg}` }, { status: 502 }); + } + } + + // ---- Default mode: use coze-coding-dev-sdk ---- + const customHeaders = HeaderUtils.extractForwardHeaders(request.headers); + const config = new Config(); + const client = new VideoGenerationClient(config, customHeaders); + + const contentItems: Array<{ type: string; text?: string; image_url?: { url: string }; role?: string }> = []; + referenceImages.forEach((url, index) => { + contentItems.push({ type: 'image_url', image_url: { url }, role: index === 0 ? 'first_frame' : 'reference' }); + }); + if (prompt) { + contentItems.push({ type: 'text', text: prompt }); + } + + const ratioMap: Record = { + '16:9': '16:9', '9:16': '9:16', '1:1': '1:1', '4:3': '4:3', '3:4': '3:4', + }; + + const response = await client.videoGeneration(contentItems as Parameters[0], { + model, + duration: Math.min(Math.max(duration, 4), 12), + ratio: ratioMap[aspectRatio] || '16:9', + resolution: '720p', + generateAudio: true, + }); + + const videos: string[] = []; + if (response.videoUrl) videos.push(response.videoUrl); + if (videos.length === 0) return NextResponse.json({ error: '视频生成失败,请稍后重试' }, { status: 500 }); + + // Persist SDK video URLs to S3 for reliable browser access + const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos'); + return NextResponse.json({ videos: persistedVideos }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '视频生成失败'; + console.error('[Video Generation Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/generation-jobs/[id]/route.ts b/src/app/api/generation-jobs/[id]/route.ts new file mode 100644 index 0000000..14b63c7 --- /dev/null +++ b/src/app/api/generation-jobs/[id]/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { getAuthenticatedUser } from '@/lib/session-auth'; +import { + buildInitialGenerationProgress, + ensureGenerationJobRuntimeSchema, + getGenerationJobEstimate, +} from '@/lib/generation-job-estimates'; + +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { + try { + const user = await getAuthenticatedUser(request); + if (!user) { + return NextResponse.json({ error: '请先登录' }, { status: 401 }); + } + + const { id } = await context.params; + if (!UUID_REGEX.test(id)) { + return NextResponse.json({ error: '任务ID格式无效' }, { status: 400 }); + } + + const client = await getDbClient(); + try { + await ensureGenerationJobRuntimeSchema(client); + await client.query( + `UPDATE generation_jobs + SET status = 'failed', + error = '任务执行超时或被服务重启中断', + payload = '{}'::jsonb, + finished_at = NOW(), + updated_at = NOW() + WHERE id = $1 + AND status = 'running' + AND updated_at < NOW() - INTERVAL '30 minutes'`, + [id], + ); + + const result = await client.query( + `SELECT id, type, status, result, error, provider, model_name, api_url, progress, + created_at, started_at, finished_at, updated_at, + CASE + WHEN started_at IS NOT NULL + THEN FLOOR(EXTRACT(EPOCH FROM (COALESCE(finished_at, NOW()) - started_at)))::int + ELSE 0 + END AS elapsed_seconds + FROM generation_jobs + WHERE id = $1 + AND (user_id = $2 OR $3 = true) + LIMIT 1`, + [id, user.userId, user.role === 'admin' || user.role === 'enterprise_admin'], + ); + + if (result.rows.length === 0) { + return NextResponse.json({ error: '任务不存在' }, { status: 404 }); + } + + const job = result.rows[0]; + const progress = job.progress && typeof job.progress === 'object' ? job.progress : {}; + const progressEstimate = Number(progress.estimateSeconds || progress.etaSeconds || 0); + let estimateSeconds = Number.isFinite(progressEstimate) && progressEstimate > 0 + ? Math.ceil(progressEstimate) + : 0; + let etaSource = typeof progress.source === 'string' ? progress.source : 'default'; + let etaSampleCount = Number(progress.sampleCount || 0); + let etaWindowDays = progress.windowDays ?? null; + + if (estimateSeconds <= 0 && (job.status === 'queued' || job.status === 'running')) { + const estimate = await getGenerationJobEstimate( + client, + job.type, + String(job.provider || ''), + String(job.model_name || ''), + ); + estimateSeconds = estimate.estimateSeconds; + etaSource = estimate.source; + etaSampleCount = estimate.sampleCount; + etaWindowDays = estimate.windowDays; + await client.query( + `UPDATE generation_jobs + SET progress = COALESCE(progress, '{}'::jsonb) || $2::jsonb, + updated_at = NOW() + WHERE id = $1`, + [id, JSON.stringify(buildInitialGenerationProgress(estimate))], + ); + } + + return NextResponse.json({ + ...job, + estimateSeconds, + eta: { + estimateSeconds, + source: etaSource, + sampleCount: etaSampleCount, + windowDays: etaWindowDays, + }, + }); + } finally { + client.release(); + } + } catch (err) { + console.error('[generation-jobs] GET error:', err); + return NextResponse.json({ error: '查询生成任务失败' }, { status: 500 }); + } +} diff --git a/src/app/api/generation-jobs/route.ts b/src/app/api/generation-jobs/route.ts new file mode 100644 index 0000000..f4bac43 --- /dev/null +++ b/src/app/api/generation-jobs/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { + markStaleRunningJobs, + processNextGenerationJob, +} from '@/lib/generation-job-worker'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; +import type { GenerationJobType } from '@/lib/generation-job-runner'; +import { + buildInitialGenerationProgress, + ensureGenerationJobRuntimeSchema, + getGenerationJobEstimate, + resolveGenerationJobIdentity, +} from '@/lib/generation-job-estimates'; +import { writePlatformLog } from '@/lib/platform-logs'; + +export async function POST(request: NextRequest) { + try { + void markStaleRunningJobs(); + const body = await request.json(); + const type = body.type as GenerationJobType; + const payload = body.payload as Record; + const userId = await getAuthenticatedUserId(request); + + if (!userId) { + return NextResponse.json({ error: '请先登录后再创建生成任务' }, { status: 401 }); + } + + if (type !== 'image' && type !== 'video') { + return NextResponse.json({ error: '不支持的任务类型' }, { status: 400 }); + } + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return NextResponse.json({ error: '缺少任务参数' }, { status: 400 }); + } + + const client = await getDbClient(); + let jobId = ''; + let estimateSeconds = type === 'video' ? 300 : 90; + let etaSource = 'default'; + let etaSampleCount = 0; + let etaWindowDays: number | null = null; + let jobIdentity = { provider: '', modelName: '', apiUrl: '' }; + try { + await ensureGenerationJobRuntimeSchema(client); + const identity = await resolveGenerationJobIdentity(client, userId, payload); + jobIdentity = identity; + const estimate = await getGenerationJobEstimate(client, type, identity.provider, identity.modelName); + estimateSeconds = estimate.estimateSeconds; + etaSource = estimate.source; + etaSampleCount = estimate.sampleCount; + etaWindowDays = estimate.windowDays; + const result = await client.query( + `INSERT INTO generation_jobs (type, status, payload, user_id, provider, model_name, api_url, progress) + VALUES ($1, 'queued', $2::jsonb, $3, $4, $5, $6, $7::jsonb) + RETURNING id`, + [ + type, + JSON.stringify(payload), + userId, + identity.provider, + identity.modelName, + identity.apiUrl, + JSON.stringify(buildInitialGenerationProgress(estimate)), + ], + ); + jobId = result.rows[0].id as string; + } finally { + client.release(); + } + + void processNextGenerationJob(); + void writePlatformLog({ + type: 'generation', + level: 'info', + action: 'generation_job_created', + message: `用户创建${type === 'image' ? '图片' : '视频'}生成任务`, + userId, + targetType: 'generation_job', + targetId: jobId, + metadata: { + type, + provider: jobIdentity.provider, + modelName: jobIdentity.modelName, + estimateSeconds, + etaSource, + etaSampleCount, + }, + request, + }); + return NextResponse.json({ + jobId, + status: 'queued', + estimateSeconds, + eta: { + estimateSeconds, + source: etaSource, + sampleCount: etaSampleCount, + windowDays: etaWindowDays, + }, + }, { status: 202 }); + } catch (err) { + console.error('[generation-jobs] POST error:', err); + return NextResponse.json({ error: '创建生成任务失败' }, { status: 500 }); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..ed6764c --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDbClient } from '@/storage/database/local-db'; + +export async function GET() { + const storageDir = process.env.LOCAL_STORAGE_DIR || path.join(process.cwd(), 'local-storage'); + const checks: Record = { + database: { ok: false }, + storage: { ok: false }, + secrets: { + ok: Boolean( + process.env.JWT_SECRET + && process.env.DATA_ENCRYPTION_KEY + && process.env.GENERATION_INTERNAL_SECRET, + ), + }, + }; + + let client: Awaited> | null = null; + try { + client = await getDbClient(); + await client.query('SELECT 1'); + checks.database.ok = true; + } catch (error) { + checks.database.message = error instanceof Error ? error.message : 'database check failed'; + } finally { + client?.release(); + } + + try { + fs.mkdirSync(storageDir, { recursive: true }); + fs.accessSync(storageDir, fs.constants.R_OK | fs.constants.W_OK); + checks.storage.ok = true; + } catch (error) { + checks.storage.message = error instanceof Error ? error.message : 'storage check failed'; + } + + const ok = Object.values(checks).every(check => check.ok); + return NextResponse.json( + { + ok, + service: 'miaojing', + role: process.env.APP_RUNTIME_ROLE || 'full', + timestamp: new Date().toISOString(), + checks, + }, + { status: ok ? 200 : 503 }, + ); +} diff --git a/src/app/api/local-storage/[...path]/route.ts b/src/app/api/local-storage/[...path]/route.ts new file mode 100644 index 0000000..782a6b3 --- /dev/null +++ b/src/app/api/local-storage/[...path]/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { localStorage } from '@/lib/local-storage'; +import path from 'path'; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + try { + const { path: pathSegments } = await params; + const filePath = normalizeStoragePath(pathSegments.join('/')); + if (!filePath) { + return NextResponse.json({ error: 'Invalid file path' }, { status: 400 }); + } + + if (!localStorage.fileExists(filePath)) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }); + } + + const fileBuffer = localStorage.readFile(filePath); + const contentType = getContentType(filePath); + + return new NextResponse(new Uint8Array(fileBuffer), { + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `inline; filename="${path.basename(filePath)}"`, + }, + }); + } catch (error) { + console.error('[Local Storage API] Error:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +function normalizeStoragePath(value: string): string | null { + try { + const decoded = decodeURIComponent(value); + const normalized = path.posix.normalize(decoded).replace(/^\/+/, ''); + if (!normalized || normalized.startsWith('..') || normalized.includes('/../') || path.isAbsolute(normalized)) { + return null; + } + return normalized; + } catch { + return null; + } +} + +function getContentType(filePath: string): string { + const extension = filePath.split('.').pop()?.toLowerCase(); + + const contentTypeMap: Record = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'webp': 'image/webp', + 'gif': 'image/gif', + 'mp4': 'video/mp4', + 'avi': 'video/x-msvideo', + 'mov': 'video/quicktime', + 'wmv': 'video/x-ms-wmv', + 'pdf': 'application/pdf', + 'txt': 'text/plain', + 'json': 'application/json', + }; + + return contentTypeMap[extension || ''] || 'application/octet-stream'; +} diff --git a/src/app/api/model-config/route.ts b/src/app/api/model-config/route.ts new file mode 100644 index 0000000..3b1bc74 --- /dev/null +++ b/src/app/api/model-config/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { listSystemApis } from '@/lib/server-api-config'; + +function mapProvider(row: Record) { + return { + id: String(row.id), + name: String(row.name || ''), + defaultApiUrl: String(row.default_api_url || ''), + defaultModel: String(row.default_model || ''), + type: String(row.type || 'image'), + website: (row.website as string | null) || null, + isActive: row.is_active !== false, + sortOrder: Number(row.sort_order || 0), + }; +} + +function mapRecommendation(row: Record) { + return { + id: String(row.id), + modelName: String(row.model_name || ''), + displayName: String(row.display_name || row.model_name || ''), + type: String(row.type || 'image'), + providerId: (row.provider_id as string | null) || null, + isActive: row.is_active !== false, + sortOrder: Number(row.sort_order || 0), + }; +} + +export async function GET() { + try { + const client = await getDbClient(); + try { + const providers = await client.query( + `SELECT id, name, default_api_url, default_model, type, website, is_active, sort_order + FROM api_providers + WHERE is_active = true + ORDER BY sort_order ASC, name ASC` + ); + const recommendations = await client.query( + `SELECT id, model_name, display_name, type, provider_id, is_active, sort_order + FROM model_recommendations + WHERE is_active = true + ORDER BY type ASC, sort_order ASC, model_name ASC` + ); + + return NextResponse.json({ + providers: providers.rows.map(mapProvider), + recommendations: recommendations.rows.map(mapRecommendation), + systemApis: await listSystemApis(false), + }); + } finally { + client.release(); + } + } catch (err) { + console.error('[model-config] GET error:', err); + return NextResponse.json({ providers: [], recommendations: [] }); + } +} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts new file mode 100644 index 0000000..e1bdec7 --- /dev/null +++ b/src/app/api/profile/route.ts @@ -0,0 +1,232 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { ensureEmailSchema } from '@/lib/email-service'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; +import { getRequiredProductionSecret } from '@/lib/runtime-env'; +import { ensureProfilePreferenceSchema } from '@/lib/profile-preferences'; + +function normalizeRoleForTier(role: string | null | undefined, tier: string | null | undefined): string { + const currentRole = role || 'user'; + if (currentRole === 'admin' || currentRole === 'enterprise_admin') return currentRole; + return tier && tier !== 'free' ? 'vip' : currentRole === 'vip' ? 'user' : currentRole; +} + +function isEmail(value: string): boolean { + return value.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); +} + +function isSafeAvatarUrl(value: string): boolean { + if (!value) return true; + if (value.length > 1_000_000) return false; + if (/^data:image\/(png|jpe?g|webp|gif);base64,[a-z0-9+/=]+$/i.test(value)) return true; + if (/^https?:\/\/[^\s"'<>]+$/i.test(value)) return true; + if (/^\/api\/local-storage\/[^\s"'<>]+$/i.test(value)) return true; + return false; +} + +function isSafeProfileText(value: string | undefined, maxLength: number): boolean { + if (value === undefined) return true; + return value.length <= maxLength && !/[\u0000-\u001f\u007f<>]/.test(value); +} + +async function verifyPasswordHash(client: Awaited>, passwordHash: string, password: string): Promise { + const result = await client.query( + 'SELECT $1::text = crypt($2::text, $1::text) AS ok', + [passwordHash, password] + ); + return result.rows[0]?.ok === true; +} + +export async function GET(request: NextRequest) { + try { + const tokenUserId = await getAuthenticatedUserId(request); + if (!tokenUserId) { + return NextResponse.json({ error: 'Please log in again' }, { status: 401 }); + } + + const client = await getDbClient(); + + try { + await ensureEmailSchema(client); + await ensureProfilePreferenceSchema(client); + const result = await client.query( + 'SELECT id, email, nickname, phone, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit, avatar_url, created_at, email_verified, email_verified_at, email_bound_at, preferred_theme FROM profiles WHERE id = $1', + [tokenUserId], + ); + + if (result.rows.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const profile = result.rows[0]; + const normalizedRole = normalizeRoleForTier(profile.role, profile.membership_tier); + if (normalizedRole !== (profile.role || 'user')) { + profile.role = normalizedRole; + await client.query('UPDATE profiles SET role = $1, updated_at = NOW() WHERE id = $2', [normalizedRole, profile.id]); + } + + return NextResponse.json({ profile }); + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to get profile'; + console.error('[Profile Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + const tokenUserId = await getAuthenticatedUserId(request); + if (!tokenUserId) { + return NextResponse.json({ error: 'Please log in again' }, { status: 401 }); + } + + try { + const body = await request.json(); + const hasEmail = Object.prototype.hasOwnProperty.call(body, 'email'); + const hasNickname = Object.prototype.hasOwnProperty.call(body, 'nickname'); + const hasPhone = Object.prototype.hasOwnProperty.call(body, 'phone'); + const hasAvatarUrl = Object.prototype.hasOwnProperty.call(body, 'avatarUrl'); + const currentPassword = typeof body.currentPassword === 'string' ? body.currentPassword : ''; + const newPassword = typeof body.newPassword === 'string' ? body.newPassword : ''; + const email = hasEmail && typeof body.email === 'string' ? body.email.trim() : undefined; + const nickname = hasNickname && typeof body.nickname === 'string' ? body.nickname.trim() : undefined; + const phone = hasPhone && typeof body.phone === 'string' ? body.phone.trim() : undefined; + const avatarUrl = hasAvatarUrl && typeof body.avatarUrl === 'string' ? body.avatarUrl.trim() : undefined; + + if (email !== undefined && !isEmail(email)) { + return NextResponse.json({ error: 'Invalid email address' }, { status: 400 }); + } + + if (!isSafeProfileText(nickname, 50)) { + return NextResponse.json({ error: 'Nickname is too long or contains invalid characters' }, { status: 400 }); + } + + if (!isSafeProfileText(phone, 30)) { + return NextResponse.json({ error: 'Phone is too long or contains invalid characters' }, { status: 400 }); + } + + if (newPassword && newPassword.length < 6) { + return NextResponse.json({ error: 'Password must be at least 6 characters' }, { status: 400 }); + } + + if (avatarUrl !== undefined && !isSafeAvatarUrl(avatarUrl)) { + return NextResponse.json({ error: 'Invalid avatar image' }, { status: 400 }); + } + + const client = await getDbClient(); + + try { + await ensureEmailSchema(client); + await ensureProfilePreferenceSchema(client); + await client.query('BEGIN'); + + const profileResult = await client.query( + 'SELECT id, email, nickname, phone, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit, avatar_url, created_at, email_verified, email_verified_at, email_bound_at, preferred_theme FROM profiles WHERE id = $1 FOR UPDATE', + [tokenUserId] + ); + + if (profileResult.rows.length === 0) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const currentProfile = profileResult.rows[0]; + const authResult = await client.query( + 'SELECT id, email, password_hash FROM auth.users WHERE id = $1 FOR UPDATE', + [tokenUserId] + ); + const authUser = authResult.rows[0] || null; + + if (email !== undefined && email !== currentProfile.email) { + const duplicateProfile = await client.query( + 'SELECT id FROM profiles WHERE email = $1 AND id <> $2 LIMIT 1', + [email, tokenUserId] + ); + const duplicateAuth = await client.query( + 'SELECT id FROM auth.users WHERE email = $1 AND id <> $2 LIMIT 1', + [email, tokenUserId] + ); + + if (duplicateProfile.rows.length > 0 || duplicateAuth.rows.length > 0) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: 'Email is already in use' }, { status: 400 }); + } + + if (authUser) { + await client.query('UPDATE auth.users SET email = $1 WHERE id = $2', [email, tokenUserId]); + } else { + await client.query( + 'INSERT INTO auth.users (id, email, created_at) VALUES ($1, $2, NOW())', + [tokenUserId, email] + ); + } + } + + if (newPassword) { + if (authUser?.password_hash) { + if (!currentPassword) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: 'Current password is required' }, { status: 400 }); + } + const passwordOk = await verifyPasswordHash(client, authUser.password_hash, currentPassword); + if (!passwordOk) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 }); + } + } else if (currentProfile.role === 'admin' && currentPassword !== getRequiredProductionSecret('ADMIN_DEFAULT_PASSWORD', 'admin123')) { + await client.query('ROLLBACK'); + return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 }); + } + + await client.query( + `INSERT INTO auth.users (id, email, password_hash, created_at) + VALUES ($1, $2, crypt($3, gen_salt('bf')), NOW()) + ON CONFLICT (id) DO UPDATE SET password_hash = crypt($3, gen_salt('bf'))`, + [tokenUserId, email || currentProfile.email, newPassword] + ); + } + + const updateResult = await client.query( + `UPDATE profiles + SET email = CASE WHEN $1::boolean THEN $2 ELSE email END, + email_verified = CASE WHEN $1::boolean AND LOWER($2) <> LOWER(email) THEN false ELSE email_verified END, + email_verified_at = CASE WHEN $1::boolean AND LOWER($2) <> LOWER(email) THEN NULL ELSE email_verified_at END, + nickname = CASE WHEN $3::boolean THEN NULLIF($4, '') ELSE nickname END, + phone = CASE WHEN $5::boolean THEN NULLIF($6, '') ELSE phone END, + avatar_url = CASE WHEN $7::boolean THEN NULLIF($8, '') ELSE avatar_url END, + updated_at = NOW() + WHERE id = $9 + RETURNING id, email, nickname, phone, role, membership_tier, credits_balance, daily_quota_used, daily_quota_limit, avatar_url, created_at, email_verified, email_verified_at, email_bound_at, preferred_theme`, + [ + email !== undefined, + email || null, + nickname !== undefined, + nickname || '', + phone !== undefined, + phone || '', + avatarUrl !== undefined, + avatarUrl || '', + tokenUserId, + ] + ); + + await client.query('COMMIT'); + + return NextResponse.json({ + success: true, + profile: updateResult.rows[0], + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to update profile'; + console.error('[Profile Update Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/profile/theme/route.ts b/src/app/api/profile/theme/route.ts new file mode 100644 index 0000000..b3a3397 --- /dev/null +++ b/src/app/api/profile/theme/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; +import { ensureProfilePreferenceSchema, normalizePreferredTheme } from '@/lib/profile-preferences'; + +export async function PUT(request: NextRequest) { + const tokenUserId = await getAuthenticatedUserId(request); + if (!tokenUserId) { + return NextResponse.json({ error: 'Please log in again' }, { status: 401 }); + } + + try { + const body = await request.json(); + const preferredTheme = normalizePreferredTheme(body?.theme); + const client = await getDbClient(); + + try { + await ensureProfilePreferenceSchema(client); + const result = await client.query( + `UPDATE profiles + SET preferred_theme = $1, + updated_at = NOW() + WHERE id = $2 + RETURNING preferred_theme`, + [preferredTheme, tokenUserId], + ); + + if (result.rows.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + return NextResponse.json({ + success: true, + preferred_theme: normalizePreferredTheme(result.rows[0].preferred_theme), + }); + } finally { + client.release(); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to save theme'; + console.error('[Profile Theme Error]', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/site-config/route.ts b/src/app/api/site-config/route.ts new file mode 100644 index 0000000..a285b49 --- /dev/null +++ b/src/app/api/site-config/route.ts @@ -0,0 +1,220 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { localStorage } from '@/lib/local-storage'; +import { requireAdmin } from '@/lib/admin-auth'; +import { DEFAULT_ABOUT_US, DEFAULT_HELP_CENTER, DEFAULT_PRIVACY_POLICY, DEFAULT_TERMS_OF_SERVICE } from '@/lib/site-policy-defaults'; +import { cleanupExpiredPlatformLogs, setPlatformLogRetentionDays, writePlatformLog } from '@/lib/platform-logs'; + +const DEFAULT_RESPONSE = { + siteName: '妙境', + siteTabTitle: '妙境 - AI创作平台', + logoUrl: null, + faviconUrl: null, + membershipEnabled: true, + termsOfService: DEFAULT_TERMS_OF_SERVICE, + privacyPolicy: DEFAULT_PRIVACY_POLICY, + aboutUs: DEFAULT_ABOUT_US, + helpCenter: DEFAULT_HELP_CENTER, + filingInfo: '', + filingUrl: '', + publicSecurityFilingInfo: '', + publicSecurityFilingUrl: '', + logRetentionDays: 30, +}; + +type SiteConfigRow = { + site_name?: string; + site_tab_title?: string; + logo_url?: string | null; + favicon_url?: string | null; + membership_enabled?: boolean; + terms_of_service?: string | null; + privacy_policy?: string | null; + about_us?: string | null; + help_center?: string | null; + filing_info?: string | null; + filing_url?: string | null; + public_security_filing_info?: string | null; + public_security_filing_url?: string | null; + log_retention_days?: number | null; +}; + +async function ensureSiteConfigColumns(client: Awaited>) { + await client.query('ALTER TABLE site_config ADD COLUMN IF NOT EXISTS membership_enabled BOOLEAN NOT NULL DEFAULT TRUE'); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS terms_of_service TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS privacy_policy TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS about_us TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS help_center TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS filing_info TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS filing_url TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS public_security_filing_info TEXT NOT NULL DEFAULT ''"); + await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS public_security_filing_url TEXT NOT NULL DEFAULT ''"); + await client.query('ALTER TABLE site_config ADD COLUMN IF NOT EXISTS log_retention_days INTEGER NOT NULL DEFAULT 30'); + await client.query('UPDATE site_config SET log_retention_days = LEAST(90, GREATEST(1, log_retention_days))'); + await client.query("UPDATE site_config SET terms_of_service = $1 WHERE terms_of_service = ''", [DEFAULT_TERMS_OF_SERVICE]); + await client.query("UPDATE site_config SET privacy_policy = $1 WHERE privacy_policy = ''", [DEFAULT_PRIVACY_POLICY]); + await client.query("UPDATE site_config SET about_us = $1 WHERE about_us = ''", [DEFAULT_ABOUT_US]); + await client.query("UPDATE site_config SET help_center = $1 WHERE help_center = ''", [DEFAULT_HELP_CENTER]); +} + +function normalizeResponse(data?: SiteConfigRow | null) { + return { + siteName: data?.site_name || DEFAULT_RESPONSE.siteName, + siteTabTitle: data?.site_tab_title || DEFAULT_RESPONSE.siteTabTitle, + logoUrl: data?.logo_url || null, + faviconUrl: data?.favicon_url || null, + membershipEnabled: data?.membership_enabled !== false, + termsOfService: data?.terms_of_service?.trim() ? data.terms_of_service : DEFAULT_TERMS_OF_SERVICE, + privacyPolicy: data?.privacy_policy?.trim() ? data.privacy_policy : DEFAULT_PRIVACY_POLICY, + aboutUs: data?.about_us?.trim() ? data.about_us : DEFAULT_ABOUT_US, + helpCenter: data?.help_center?.trim() ? data.help_center : DEFAULT_HELP_CENTER, + filingInfo: data?.filing_info?.trim() || '', + filingUrl: data?.filing_url?.trim() || '', + publicSecurityFilingInfo: data?.public_security_filing_info?.trim() || '', + publicSecurityFilingUrl: data?.public_security_filing_url?.trim() || '', + logRetentionDays: Math.min(90, Math.max(1, Number(data?.log_retention_days || 30))), + }; +} + +function decodeDataImage(value: unknown): { buffer: Buffer; ext: string; contentType: string } | null { + if (typeof value !== 'string') return null; + const match = value.match(/^data:image\/(png|jpe?g|webp|gif|svg\+xml);base64,([a-z0-9+/=]+)$/i); + if (!match) return null; + const subtype = match[1].toLowerCase(); + const ext = subtype === 'jpeg' ? 'jpg' : subtype === 'svg+xml' ? 'svg' : subtype; + const buffer = Buffer.from(match[2], 'base64'); + if (buffer.length <= 0 || buffer.length > 3 * 1024 * 1024) return null; + return { buffer, ext, contentType: `image/${subtype}` }; +} + +async function saveImageDataUrl(value: unknown, prefix: string): Promise { + const decoded = decodeDataImage(value); + if (!decoded) return null; + const key = `site-assets/${prefix}-${Date.now()}.${decoded.ext}`; + const savedKey = await localStorage.uploadFile({ + fileContent: decoded.buffer, + fileName: key, + contentType: decoded.contentType, + }); + return localStorage.generatePresignedUrl({ key: savedKey, expireTime: 31536000 }); +} + +export async function GET() { + try { + const client = await getDbClient(); + try { + await ensureSiteConfigColumns(client); + const result = await client.query( + 'SELECT site_name, site_tab_title, logo_url, favicon_url, membership_enabled, terms_of_service, privacy_policy, about_us, help_center, filing_info, filing_url, public_security_filing_info, public_security_filing_url, log_retention_days FROM site_config WHERE id = 1' + ); + + if (result.rows.length === 0) { + return NextResponse.json(DEFAULT_RESPONSE); + } + + return NextResponse.json(normalizeResponse(result.rows[0])); + } finally { + client.release(); + } + } catch { + return NextResponse.json(DEFAULT_RESPONSE); + } +} + +export async function PUT(request: NextRequest) { + const authError = await requireAdmin(request); + if (authError) return authError; + + try { + const body = await request.json(); + if (!body) { + return NextResponse.json({ error: '无效的请求体' }, { status: 400 }); + } + + const { siteName, siteTabTitle, membershipEnabled, logoBase64, faviconBase64, termsOfService, privacyPolicy, aboutUs, helpCenter, filingInfo, filingUrl, publicSecurityFilingInfo, publicSecurityFilingUrl, logRetentionDays } = body as { + siteName?: string; + siteTabTitle?: string; + membershipEnabled?: boolean; + logoBase64?: string; + faviconBase64?: string; + termsOfService?: string; + privacyPolicy?: string; + aboutUs?: string; + helpCenter?: string; + filingInfo?: string; + filingUrl?: string; + publicSecurityFilingInfo?: string; + publicSecurityFilingUrl?: string; + logRetentionDays?: number; + }; + + const client = await getDbClient(); + try { + await ensureSiteConfigColumns(client); + const updates: string[] = []; + const params: unknown[] = []; + let paramIdx = 1; + + if (typeof siteName === 'string') { updates.push(`site_name = $${paramIdx++}`); params.push(siteName); } + if (typeof siteTabTitle === 'string') { updates.push(`site_tab_title = $${paramIdx++}`); params.push(siteTabTitle); } + if (typeof membershipEnabled === 'boolean') { updates.push(`membership_enabled = $${paramIdx++}`); params.push(membershipEnabled); } + if (typeof termsOfService === 'string') { updates.push(`terms_of_service = $${paramIdx++}`); params.push(termsOfService.trim() || DEFAULT_TERMS_OF_SERVICE); } + if (typeof privacyPolicy === 'string') { updates.push(`privacy_policy = $${paramIdx++}`); params.push(privacyPolicy.trim() || DEFAULT_PRIVACY_POLICY); } + if (typeof aboutUs === 'string') { updates.push(`about_us = $${paramIdx++}`); params.push(aboutUs.trim() || DEFAULT_ABOUT_US); } + if (typeof helpCenter === 'string') { updates.push(`help_center = $${paramIdx++}`); params.push(helpCenter.trim() || DEFAULT_HELP_CENTER); } + if (typeof filingInfo === 'string') { updates.push(`filing_info = $${paramIdx++}`); params.push(filingInfo.trim()); } + if (typeof filingUrl === 'string') { updates.push(`filing_url = $${paramIdx++}`); params.push(filingUrl.trim()); } + if (typeof publicSecurityFilingInfo === 'string') { updates.push(`public_security_filing_info = $${paramIdx++}`); params.push(publicSecurityFilingInfo.trim()); } + if (typeof publicSecurityFilingUrl === 'string') { updates.push(`public_security_filing_url = $${paramIdx++}`); params.push(publicSecurityFilingUrl.trim()); } + if (typeof logRetentionDays === 'number') { + const safeLogRetentionDays = Math.min(90, Math.max(1, Math.floor(logRetentionDays))); + updates.push(`log_retention_days = $${paramIdx++}`); + params.push(safeLogRetentionDays); + await setPlatformLogRetentionDays(client, safeLogRetentionDays); + await cleanupExpiredPlatformLogs(client); + } + const logoUrl = await saveImageDataUrl(logoBase64, 'logo'); + const faviconUrl = await saveImageDataUrl(faviconBase64, 'favicon'); + if (logoUrl) { updates.push(`logo_url = $${paramIdx++}`); params.push(logoUrl); } + if (faviconUrl) { updates.push(`favicon_url = $${paramIdx++}`); params.push(faviconUrl); } + updates.push(`updated_at = NOW()`); + + if (updates.length > 1) { + await client.query( + "INSERT INTO site_config (id, site_name, site_tab_title) VALUES (1, '妙境', '妙境 - AI创作平台') ON CONFLICT (id) DO NOTHING" + ); + await client.query( + `UPDATE site_config SET ${updates.join(', ')} WHERE id = 1`, + params + ); + } + + const result = await client.query( + 'SELECT site_name, site_tab_title, logo_url, favicon_url, membership_enabled, terms_of_service, privacy_policy, about_us, help_center, filing_info, filing_url, public_security_filing_info, public_security_filing_url, log_retention_days FROM site_config WHERE id = 1' + ); + + void writePlatformLog({ + type: 'admin', + level: 'info', + action: 'site_config_updated', + message: '管理员更新了系统设置', + targetType: 'site_config', + targetId: '1', + metadata: { + fields: updates + .filter(item => !item.startsWith('updated_at')) + .map(item => item.split('=')[0]?.trim()) + .filter(Boolean), + }, + request, + }); + + return NextResponse.json(normalizeResponse(result.rows[0])); + } finally { + client.release(); + } + } catch (err) { + console.error('[site-config] PUT error:', err); + return NextResponse.json({ error: '服务器错误' }, { status: 500 }); + } +} diff --git a/src/app/api/site-stats/route.ts b/src/app/api/site-stats/route.ts new file mode 100644 index 0000000..a584159 --- /dev/null +++ b/src/app/api/site-stats/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; + +export async function GET() { + try { + const client = await getDbClient(); + try { + const result = await client.query('SELECT total_visits FROM site_stats WHERE id = 1'); + return NextResponse.json({ totalVisits: result.rows[0]?.total_visits || 0 }); + } finally { + client.release(); + } + } catch { + return NextResponse.json({ totalVisits: 0 }); + } +} + +export async function POST() { + try { + const client = await getDbClient(); + try { + const result = await client.query('SELECT increment_visits() as new_count'); + return NextResponse.json({ totalVisits: result.rows[0]?.new_count || 0 }); + } finally { + client.release(); + } + } catch { + try { + const client = await getDbClient(); + try { + await client.query('UPDATE site_stats SET total_visits = total_visits + 1, updated_at = NOW() WHERE id = 1'); + const result = await client.query('SELECT total_visits FROM site_stats WHERE id = 1'); + return NextResponse.json({ totalVisits: result.rows[0]?.total_visits || 0 }); + } finally { + client.release(); + } + } catch { + return NextResponse.json({ totalVisits: 0 }); + } + } +} \ No newline at end of file diff --git a/src/app/api/user-api-keys/route.ts b/src/app/api/user-api-keys/route.ts new file mode 100644 index 0000000..1cfd1c8 --- /dev/null +++ b/src/app/api/user-api-keys/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDbClient } from '@/storage/database/local-db'; +import { decryptSecret, encryptSecret, previewSecret } from '@/lib/server-crypto'; +import { getAuthenticatedUserId } from '@/lib/session-auth'; + +function normalizeType(value: unknown): 'image' | 'video' | 'text' { + return value === 'video' || value === 'text' ? value : 'image'; +} + +function mapKey(row: Record) { + const apiKey = decryptSecret(row.api_key_encrypted as string); + return { + id: row.id, + provider: row.provider || '', + supplierName: row.supplier_name || row.provider || '', + apiUrl: row.api_url || '', + modelName: row.model_name || '', + apiKey: '', + apiKeyPreview: row.api_key_preview || previewSecret(apiKey), + type: normalizeType(row.type), + isActive: row.is_active !== false, + createdAt: row.created_at, + }; +} + +export async function GET(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + + const client = await getDbClient(); + try { + await client.query(` + ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128); + ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image'; + `); + const result = await client.query( + `SELECT id, provider, supplier_name, api_url, model_name, api_key_encrypted, api_key_preview, type, is_active, created_at + FROM user_api_keys + WHERE user_id = $1 + ORDER BY created_at DESC`, + [userId], + ); + return NextResponse.json({ keys: result.rows.map(mapKey) }); + } finally { + client.release(); + } +} + +export async function POST(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + const body = await request.json(); + const keys = Array.isArray(body.keys) ? body.keys : [body]; + + const client = await getDbClient(); + try { + await client.query('BEGIN'); + await client.query(` + ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128); + ALTER TABLE user_api_keys ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image'; + `); + const saved = []; + for (const item of keys) { + const apiKey = String(item.apiKey || '').trim(); + const id = typeof item.id === 'string' && /^[0-9a-fA-F-]{36}$/.test(item.id) ? item.id : undefined; + if (!apiKey && !id) continue; + const values = [ + userId, + String(item.provider || '').trim(), + String(item.supplierName || item.provider || '').trim(), + String(item.apiUrl || '').trim(), + String(item.modelName || '').trim(), + apiKey ? encryptSecret(apiKey) : null, + apiKey ? previewSecret(apiKey) : null, + normalizeType(item.type), + item.isActive !== false, + ]; + const result = await client.query( + id + ? `UPDATE user_api_keys + SET provider = $2, + supplier_name = $3, + api_url = $4, + model_name = $5, + api_key_encrypted = COALESCE($6, api_key_encrypted), + api_key_preview = COALESCE($7, api_key_preview), + type = $8, + is_active = $9, + updated_at = NOW() + WHERE id = $10 AND user_id = $1 + RETURNING *` + : `INSERT INTO user_api_keys (user_id, provider, supplier_name, api_url, model_name, api_key_encrypted, api_key_preview, type, is_active, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) + RETURNING *`, + id ? [...values, id] : values, + ); + saved.push(mapKey(result.rows[0])); + } + await client.query('COMMIT'); + return NextResponse.json({ keys: saved }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +export async function PUT(request: NextRequest) { + return POST(request); +} + +export async function DELETE(request: NextRequest) { + const userId = await getAuthenticatedUserId(request); + if (!userId) return NextResponse.json({ error: '请先登录' }, { status: 401 }); + const id = request.nextUrl.searchParams.get('id'); + if (!id) return NextResponse.json({ error: '缺少 ID' }, { status: 400 }); + const client = await getDbClient(); + try { + await client.query('DELETE FROM user_api_keys WHERE id = $1 AND user_id = $2', [id, userId]); + return NextResponse.json({ success: true }); + } finally { + client.release(); + } +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..fe768c3 --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,530 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useTheme } from 'next-themes'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Brush, Mail, Lock, User, Phone, Eye, EyeOff, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { useAuth, parseApiUser } from '@/lib/auth-store'; +import { RegistrationAgreementDialog } from '@/components/auth/registration-agreement-dialog'; + +const EMAIL_REGEX = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/; +const authInputIconClass = 'pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-foreground/70 dark:text-foreground/80'; +const authPasswordToggleClass = 'absolute right-3 top-1/2 z-10 -translate-y-1/2 text-foreground/70 transition-colors hover:text-foreground dark:text-foreground/80'; + +function isEmail(value: string) { + return EMAIL_REGEX.test(value.trim()); +} + +function sanitizeCode(value: string) { + return value.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 10); +} + +function isStrongPassword(value: string) { + return value.length >= 8 && /[a-zA-Z]/.test(value) && /\d/.test(value); +} + +export default function AuthPage() { + const router = useRouter(); + const { login } = useAuth(); + const { setTheme } = useTheme(); + const [activeTab, setActiveTab] = useState('login'); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + // Login form + const [loginAccount, setLoginAccount] = useState(''); + const [loginPassword, setLoginPassword] = useState(''); + + // Register form + const [regEmail, setRegEmail] = useState(''); + const [regPassword, setRegPassword] = useState(''); + const [regNickname, setRegNickname] = useState(''); + const [regPhone, setRegPhone] = useState(''); + const [regInviteCode, setRegInviteCode] = useState(''); + const [regEmailCode, setRegEmailCode] = useState(''); + const [regCodeCooldown, setRegCodeCooldown] = useState(0); + const [sendingRegCode, setSendingRegCode] = useState(false); + const [showInviteCode, setShowInviteCode] = useState(false); + const [showForgotPw, setShowForgotPw] = useState(false); + const [resetEmail, setResetEmail] = useState(''); + const [resetCode, setResetCode] = useState(''); + const [resetPassword, setResetPassword] = useState(''); + const [resetConfirmPassword, setResetConfirmPassword] = useState(''); + const [resetCooldown, setResetCooldown] = useState(0); + const [sendingResetCode, setSendingResetCode] = useState(false); + const [resettingPassword, setResettingPassword] = useState(false); + const [showAgreement, setShowAgreement] = useState(false); + + // Auto-initialize default admin account on mount (fire-and-forget) + useEffect(() => { + fetch('/api/auth/admin-exists').catch(() => {/* silent */}); + }, []); + + useEffect(() => { + if (regCodeCooldown <= 0) return; + const timer = window.setInterval(() => setRegCodeCooldown(prev => Math.max(0, prev - 1)), 1000); + return () => window.clearInterval(timer); + }, [regCodeCooldown]); + + useEffect(() => { + if (resetCooldown <= 0) return; + const timer = window.setInterval(() => setResetCooldown(prev => Math.max(0, prev - 1)), 1000); + return () => window.clearInterval(timer); + }, [resetCooldown]); + + const handleLogin = async () => { + if (!loginAccount || !loginPassword) { + toast.error('请填写账号和密码'); + return; + } + setLoading(true); + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account: loginAccount, password: loginPassword }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || '登录失败'); + + // Save auth state with full profile + const authUser = parseApiUser(data.user || {}); + login(authUser, data.session?.access_token || ''); + setTheme(authUser.preferredTheme); + + toast.success('登录成功'); + router.push('/create'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '登录失败'); + } finally { + setLoading(false); + } + }; + + const handleRegister = async () => { + if (!regEmail || !regPassword) { + toast.error('请填写邮箱和密码'); + return; + } + if (!isEmail(regEmail)) { + toast.error('请输入正确的邮箱地址'); + return; + } + setLoading(true); + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: regEmail, + password: regPassword, + nickname: regNickname, + phone: regPhone, + inviteCode: regInviteCode || undefined, + emailCode: showInviteCode && regInviteCode ? undefined : regEmailCode, + acceptedTerms: true, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || '注册失败'); + + // Save auth state with full profile + const authUser = parseApiUser(data.user || {}); + login(authUser, data.session?.access_token || ''); + setTheme(authUser.preferredTheme); + + toast.success(data.message || '注册成功'); + router.push('/create'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '注册失败'); + } finally { + setLoading(false); + } + }; + + const requestRegisterAgreement = () => { + setShowAgreement(true); + }; + + const handleAgreeAndRegister = () => { + setShowAgreement(false); + handleRegister(); + }; + + const handleSendRegisterCode = async () => { + if (!isEmail(regEmail)) { + toast.error('请输入正确的邮箱地址'); + return; + } + setSendingRegCode(true); + try { + const res = await fetch('/api/email/send-register-code', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: regEmail }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || '验证码发送失败'); + setRegCodeCooldown(data.cooldown || 60); + toast.success(data.message || '验证码已发送'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '验证码发送失败'); + } finally { + setSendingRegCode(false); + } + }; + + const handleSendResetCode = async () => { + if (!isEmail(resetEmail)) { + toast.error('请输入注册时绑定并验证过的邮箱'); + return; + } + setSendingResetCode(true); + try { + const res = await fetch('/api/email/send-reset-code', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: resetEmail }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || '验证码发送失败'); + setResetCooldown(data.cooldown || 60); + toast.success(data.message || '如果该邮箱可用于重置,我们已发送验证码'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '验证码发送失败'); + } finally { + setSendingResetCode(false); + } + }; + + const handleResetPassword = async () => { + if (!isEmail(resetEmail) || !resetCode) { + toast.error('请填写邮箱和验证码'); + return; + } + if (!isStrongPassword(resetPassword)) { + toast.error('新密码至少 8 位,并同时包含字母和数字'); + return; + } + if (resetPassword !== resetConfirmPassword) { + toast.error('两次输入的新密码不一致'); + return; + } + setResettingPassword(true); + try { + const res = await fetch('/api/email/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: resetEmail, code: resetCode, newPassword: resetPassword }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || '密码重置失败'); + toast.success(data.message || '密码已重置,请重新登录'); + setShowForgotPw(false); + setLoginAccount(resetEmail); + setResetCode(''); + setResetPassword(''); + setResetConfirmPassword(''); + } catch (err) { + toast.error(err instanceof Error ? err.message : '密码重置失败'); + } finally { + setResettingPassword(false); + } + }; + + return ( +
+
+
+
+ +
+ {/* Logo */} +
+ +
+ +
+ 妙境 + +

妙手丹青,境随心造

+
+ + + + + + 登录 + 注册 + + + + + +
+ +
+ + setLoginAccount(e.target.value)} + className="pl-10" + onKeyDown={(e) => { if (e.key === 'Enter') handleLogin(); }} + /> +
+
+
+ +
+ + setLoginPassword(e.target.value)} + className="pl-10 pr-10" + onKeyDown={(e) => { if (e.key === 'Enter') handleLogin(); }} + /> + +
+ +
+ +
+ + +
+ +
+ + setRegEmail(e.target.value)} + className="pl-10" + /> +
+
+ {!(showInviteCode && regInviteCode) && ( +
+ +
+ setRegEmailCode(sanitizeCode(e.target.value))} + className="uppercase" + maxLength={10} + /> + +
+
+ )} +
+ +
+ + setRegNickname(e.target.value)} + className="pl-10" + /> +
+
+
+ +
+ + setRegPhone(e.target.value)} + className="pl-10" + /> +
+
+
+ +
+ + setRegPassword(e.target.value)} + className="pl-10 pr-10" + onKeyDown={(e) => { if (e.key === 'Enter') requestRegisterAgreement(); }} + /> + +
+
+ {/* Admin invite code */} +
+
+ + +
+ {showInviteCode && ( +
+ + setRegInviteCode(e.target.value)} + className="pl-10" + /> +
+ )} +
+ +

+ 注册即表示同意 + + 服务条款 + + 和 + + 隐私政策 + +

+
+
+
+
+ + {/* Forgot Password Dialog */} + + + + 忘记密码 + + 输入已验证邮箱,获取验证码后设置新密码。 + + +
+
+ + setResetEmail(e.target.value)} + /> +
+
+ +
+ setResetCode(sanitizeCode(e.target.value))} + className="uppercase" + maxLength={10} + /> + +
+
+
+ + setResetPassword(e.target.value)} + /> +
+
+ + setResetConfirmPassword(e.target.value)} + /> +
+
+
+ + +
+
+
+ + +
+
+ ); +} diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx new file mode 100644 index 0000000..944caa1 --- /dev/null +++ b/src/app/auth/register/page.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Brush, Mail, Lock, User, Phone, Eye, EyeOff, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { addCreditRecord } from '@/lib/credit-records-store'; +import { RegistrationAgreementDialog } from '@/components/auth/registration-agreement-dialog'; + +const EMAIL_REGEX = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/; +const authInputIconClass = 'pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-foreground/70 dark:text-foreground/80'; +const authPasswordToggleClass = 'absolute right-3 top-1/2 z-10 -translate-y-1/2 text-foreground/70 transition-colors hover:text-foreground dark:text-foreground/80'; + +function isEmail(value: string) { + return EMAIL_REGEX.test(value.trim()); +} + +function sanitizeCode(value: string) { + return value.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 10); +} + +function isStrongPassword(value: string) { + return value.length >= 8 && /[A-Za-z]/.test(value) && /\d/.test(value); +} + +export default function RegisterPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [nickname, setNickname] = useState(''); + const [phone, setPhone] = useState(''); + const [emailCode, setEmailCode] = useState(''); + const [sendingCode, setSendingCode] = useState(false); + const [codeCooldown, setCodeCooldown] = useState(0); + const [showAgreement, setShowAgreement] = useState(false); + + useEffect(() => { + if (codeCooldown <= 0) return; + const timer = window.setInterval(() => setCodeCooldown(prev => Math.max(0, prev - 1)), 1000); + return () => window.clearInterval(timer); + }, [codeCooldown]); + + const handleRegister = async () => { + if (!email || !password) { + toast.error('请填写邮箱和密码'); + return; + } + if (!isEmail(email)) { + toast.error('请输入正确的邮箱地址'); + return; + } + if (!isStrongPassword(password)) { + toast.error('密码至少 8 位,并同时包含字母和数字'); + return; + } + if (!emailCode) { + toast.error('请输入邮箱验证码'); + return; + } + setLoading(true); + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, nickname, phone, emailCode, acceptedTerms: true }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || '注册失败'); + toast.success('注册成功,赠送10积分体验金'); + addCreditRecord({ type: 'gift', amount: 10, balanceAfter: 10, description: '新用户注册奖励' }); + router.push('/create'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '注册失败'); + } finally { + setLoading(false); + } + }; + + const requestRegisterAgreement = () => { + setShowAgreement(true); + }; + + const handleAgreeAndRegister = () => { + setShowAgreement(false); + handleRegister(); + }; + + const handleSendCode = async () => { + if (!isEmail(email)) { + toast.error('请输入正确的邮箱地址'); + return; + } + setSendingCode(true); + try { + const res = await fetch('/api/email/send-register-code', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || '验证码发送失败'); + setCodeCooldown(data.cooldown || 60); + toast.success(data.message || '验证码已发送'); + } catch (err) { + toast.error(err instanceof Error ? err.message : '验证码发送失败'); + } finally { + setSendingCode(false); + } + }; + + return ( +
+
+
+
+
+
+ +
+ +
+ 妙境 + +
+ + + + 创建账号 + 注册即可获得10积分体验金 + + +
+ +
+ + setEmail(e.target.value)} className="pl-10" /> +
+
+
+ +
+ setEmailCode(sanitizeCode(e.target.value))} + className="uppercase" + maxLength={10} + /> + +
+
+
+ +
+ + setNickname(e.target.value)} className="pl-10" /> +
+
+
+ +
+ + setPhone(e.target.value)} className="pl-10" /> +
+
+
+ +
+ + setPassword(e.target.value)} className="pl-10 pr-10" /> + +
+
+ +

+ 注册即表示同意 + + 服务条款 + + 和 + + 隐私政策 + +

+

+ 已有账号? 去登录 +

+
+
+
+ +
+ ); +} diff --git a/src/app/console/dashboard/page.tsx b/src/app/console/dashboard/page.tsx new file mode 100644 index 0000000..aa34de0 --- /dev/null +++ b/src/app/console/dashboard/page.tsx @@ -0,0 +1,3 @@ +import { ConsoleDashboardPage } from '@/modules/console'; + +export default ConsoleDashboardPage; diff --git a/src/app/console/page.tsx b/src/app/console/page.tsx new file mode 100644 index 0000000..8c270f7 --- /dev/null +++ b/src/app/console/page.tsx @@ -0,0 +1,3 @@ +import { ConsoleLoginPage } from '@/modules/console'; + +export default ConsoleLoginPage; diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx new file mode 100644 index 0000000..c06cf2e --- /dev/null +++ b/src/app/create/page.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { Suspense, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { TextToImagePanel } from '@/components/create/text-to-image'; +import { ImageToImagePanel } from '@/components/create/image-to-image'; +import { TextToVideoPanel } from '@/components/create/text-to-video'; +import { ImageToVideoPanel } from '@/components/create/image-to-video'; +import ReversePromptPanel from '@/components/create/reverse-prompt-panel'; +import { Brush, ImagePlus, Video, Film, Loader2, FileSearch } from 'lucide-react'; + +function CreateContent() { + const searchParams = useSearchParams(); + const typeParam = searchParams.get('type') || 'text2img'; + + const typeMap: Record = { + text2img: 'text2img', + img2img: 'img2img', + text2video: 'text2video', + img2video: 'img2video', + reversePrompt: 'reversePrompt', + }; + + const [activeTab, setActiveTab] = useState(typeMap[typeParam] || 'text2img'); + + return ( + + + + + 文生图 + + + + 图生图 + + + + + + 图生视频 + + + + 图片反推 + + + + + + + + + + + + + + + + + setActiveTab('text2img')} + onUseForImageToImage={() => setActiveTab('img2img')} + /> + + + ); +} + +export default function CreatePage() { + return ( +
+
+
+

创作中心

+

+ 选择创作模式,释放你的想象力 +

+
+
}> + + +
+
+ ); +} diff --git a/src/app/gallery/page.tsx b/src/app/gallery/page.tsx new file mode 100644 index 0000000..4b72ffd --- /dev/null +++ b/src/app/gallery/page.tsx @@ -0,0 +1,910 @@ +'use client'; + +import { useState, useMemo, useEffect, useCallback } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + LayoutGrid, + Heart, + Download, + Brush, + ImagePlus, + Video, + Film, + X, + Clock, + Cpu, + Sparkles, + Image as ImageIcon, + MessageSquare, + Copy, + Maximize2, + ArrowLeft, + Trash2, + Search, +} from 'lucide-react'; +import { copyTextToClipboard, downloadFile } from '@/lib/utils'; +import { usePublishedWorks, useCreationHistory, syncPublishedToSupabase, type PublishedWork } from '@/lib/creation-history-store'; +import { useAuth } from '@/lib/auth-store'; +import { FullscreenPreview } from '@/components/fullscreen-preview'; +import { toast } from 'sonner'; + +const CATEGORIES = [ + { value: 'all', label: '全部', icon: LayoutGrid }, + { value: 'text2img', label: '文生图', icon: Brush }, + { value: 'img2img', label: '图生图', icon: ImagePlus }, + { value: 'text2video', label: '文生视频', icon: Video }, + { value: 'img2video', label: '图生视频', icon: Film }, +]; + +/* ---------- Gallery Work (from API) ---------- */ +interface GalleryWork { + id: string; + type: string; + title?: string | null; + prompt?: string | null; + negativePrompt?: string | null; + url: string; + thumbnailUrl?: string | null; + width?: number | null; + height?: number | null; + duration?: number | null; + likes: number; + creditsCost?: number | null; + params: Record; + referenceImage?: string | null; + referenceImages?: string[]; + publisherId: string; + publisherNickname: string; + publisherAvatarUrl?: string | null; + publishedAt: string; +} + +function getCategoryFromWork(work: GalleryWork): string { + const mode = work.params?.creationMode || work.params?.workType || work.params?.mode; + if ( + mode === 'text2img' || + mode === 'img2img' || + mode === 'text2video' || + mode === 'img2video' + ) { + return mode; + } + if (work.type === 'text2video' || work.type === 'img2video') { + return work.type; + } + if (work.type === 'img2img') return work.type; + const hasReference = + Boolean(work.referenceImage) || + (Array.isArray(work.referenceImages) && work.referenceImages.length > 0) || + Boolean(work.params?.referenceImage) || + (Array.isArray(work.params?.referenceImages) && work.params.referenceImages.length > 0); + // Fallback: infer from type + referenceImage + if (work.type === 'video' || work.duration) { + return hasReference ? 'img2video' : 'text2video'; + } + return hasReference ? 'img2img' : 'text2img'; +} + +function getCategoryLabel(work: GalleryWork): string { + const cat = CATEGORIES.find(c => c.value === getCategoryFromWork(work)); + return cat?.label ?? work.type; +} + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return iso; + } +} + +function getAvatarText(nickname: string): string { + const trimmed = nickname.trim(); + return trimmed ? trimmed.slice(0, 1).toUpperCase() : '匿'; +} + +function getWorkReferenceImages(work: GalleryWork): string[] { + const fromArray = Array.isArray(work.referenceImages) ? work.referenceImages : []; + const fromParams = Array.isArray(work.params?.referenceImages) + ? (work.params.referenceImages as unknown[]).filter((item): item is string => typeof item === 'string' && item.trim().length > 0) + : []; + const single = typeof work.referenceImage === 'string' && work.referenceImage.trim() + ? [work.referenceImage] + : typeof work.params?.referenceImage === 'string' && work.params.referenceImage.trim() + ? [work.params.referenceImage] + : []; + return [...new Set([...single, ...fromArray, ...fromParams].filter(url => url && !url.startsWith('data:') && !url.startsWith('[')))]; +} + +async function copyGalleryText(text: string, successMessage: string) { + const copied = await copyTextToClipboard(text); + if (copied) { + toast.success(successMessage); + } else { + toast.error('复制失败,请手动选择文本复制'); + } +} + +function getCreateUrlForCategory(category: string): string { + const type = category === 'img2img' || category === 'text2video' || category === 'img2video' + ? category + : 'text2img'; + return `/create?type=${type}`; +} + +type MediaSize = { width: number; height: number }; + +function getEstimatedWorkHeight(work: GalleryWork, measuredSize?: MediaSize): number { + const width = Number(measuredSize?.width || work.width || 0); + const height = Number(measuredSize?.height || work.height || 0); + const imageHeight = width > 0 && height > 0 ? Math.max(120, (height / width) * 320) : 320; + return imageHeight + 152 + 16; +} + +const galleryGlassPanel = + 'liquid-glass'; +const galleryGlassCard = + 'liquid-surface'; +const galleryGlassBlock = + 'rounded-xl border border-border bg-card/40'; +const detailGlassBlock = + 'rounded-xl border border-white/[0.08] bg-[#12161d]/82 shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_16px_36px_rgba(0,0,0,0.18)] backdrop-blur-xl light:border-amber-900/18 light:bg-white/36 light:text-foreground light:shadow-[inset_0_1px_0_rgba(255,255,255,0.70),0_16px_40px_rgba(83,61,27,0.12)]'; +const detailGlassInner = + 'rounded-md border border-white/[0.07] bg-[#0d1219]/80 light:border-amber-900/16 light:bg-white/32'; +const galleryMenuItemClass = + 'inline-flex h-10 cursor-pointer items-center gap-2.5 rounded-xl border border-transparent px-5 text-base font-semibold leading-none text-foreground/75 transition-colors hover:bg-white/[0.035]'; +const galleryMenuItemActiveClass = + 'border-transparent bg-white/[0.075] text-primary shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_0_18px_rgba(244,166,36,0.18),0_6px_18px_rgba(0,0,0,0.18)] [&_svg]:text-primary'; + +export default function GalleryPage() { + const [apiWorks, setApiWorks] = useState([]); + const [loading, setLoading] = useState(true); + const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'done'>('idle'); + const [category, setCategory] = useState('all'); + const [likedIds, setLikedIds] = useState>(new Set()); + const [selectedWork, setSelectedWork] = useState(null); + const [fullscreenSrc, setFullscreenSrc] = useState(null); + const [sortBy, setSortBy] = useState<'newest' | 'popular'>('newest'); + const [searchQuery, setSearchQuery] = useState(''); + const [masonryColumnCount, setMasonryColumnCount] = useState(4); + const [measuredMediaSizes, setMeasuredMediaSizes] = useState>({}); + const [selectedGalleryIds, setSelectedGalleryIds] = useState>(new Set()); + + useEffect(() => { + const updateColumnCount = () => { + const width = window.innerWidth; + if (width >= 1280) setMasonryColumnCount(4); + else if (width >= 1024) setMasonryColumnCount(3); + else if (width >= 640) setMasonryColumnCount(2); + else setMasonryColumnCount(1); + }; + + updateColumnCount(); + window.addEventListener('resize', updateColumnCount); + return () => window.removeEventListener('resize', updateColumnCount); + }, []); + + // ESC to close detail overlay + useEffect(() => { + if (!selectedWork) return; + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setSelectedWork(null); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [selectedWork]); + + // Prevent body scroll when detail is open + useEffect(() => { + if (selectedWork) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { document.body.style.overflow = ''; }; + }, [selectedWork]); + const { works: localPublished } = usePublishedWorks(); + const { records: creationHistory } = useCreationHistory(); + const { user, accessToken, isAdmin } = useAuth(); + + // Fetch works from API, after syncing localStorage to Supabase + const fetchWorks = useCallback(async () => { + setLoading(true); + try { + const params = new URLSearchParams({ sort: sortBy, limit: '300' }); + if (searchQuery.trim()) params.set('q', searchQuery.trim()); + const res = await fetch(`/api/gallery?${params.toString()}`); + if (res.ok) { + const data = await res.json(); + setApiWorks(data.works || []); + } + } catch { /* ignore */ } + setLoading(false); + }, [sortBy, searchQuery]); + + // Sync localStorage to Supabase on first mount only + useEffect(() => { + setSyncStatus('syncing'); + syncPublishedToSupabase().then(synced => { + setSyncStatus('done'); + if (synced > 0) { + // Re-fetch after sync to show newly synced works + fetchWorks(); + } + }).catch(() => { + setSyncStatus('done'); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + fetchWorks(); + }, [fetchWorks]); + + // Merge API works with localStorage published works + published creation history + // This ensures previously shared works are visible even if not yet in Supabase + const works = useMemo(() => { + const apiUrls = new Set(apiWorks.map(w => w.url)); + + // From localStorage published gallery + const localAsGallery: GalleryWork[] = localPublished + .filter(w => !apiUrls.has(w.url)) + .map(w => ({ + id: w.id, + type: w.type === 'video' ? (w.referenceImage ? 'img2video' : 'text2video') : (w.referenceImage ? 'img2img' : 'text2img'), + title: null, + prompt: w.prompt, + negativePrompt: w.negativePrompt, + url: w.url, + thumbnailUrl: null, + width: null, + height: null, + duration: null, + likes: w.likes || 0, + creditsCost: null, + params: { model: w.model, modelLabel: w.modelLabel, ...w.params }, + referenceImage: w.referenceImage, + referenceImages: w.referenceImages, + publisherId: w.publisherId, + publisherNickname: w.publisherNickname, + publisherAvatarUrl: null, + publishedAt: w.publishedAt, + })); + + // From creation history records marked as published + const existingUrls = new Set([...apiUrls, ...localAsGallery.map(w => w.url)]); + const historyPublished: GalleryWork[] = creationHistory + .filter(r => r.published && r.url && !existingUrls.has(r.url) && !r.url.startsWith('data:') && !r.url.startsWith('[')) + .map(r => ({ + id: r.id, + type: r.type === 'video' ? (r.referenceImage ? 'img2video' : 'text2video') : (r.referenceImage ? 'img2img' : 'text2img'), + title: null, + prompt: r.prompt, + negativePrompt: r.negativePrompt, + url: r.url, + thumbnailUrl: null, + width: null, + height: null, + duration: null, + likes: 0, + creditsCost: null, + params: { model: r.model, modelLabel: r.modelLabel, ...r.params }, + referenceImage: r.referenceImage, + referenceImages: r.referenceImages, + publisherId: user?.id || 'anonymous', + publisherNickname: user?.nickname || user?.email?.split('@')[0] || '匿名用户', + publisherAvatarUrl: user?.avatarUrl || null, + publishedAt: r.createdAt, + })); + + return [...apiWorks, ...localAsGallery, ...historyPublished]; + }, [apiWorks, localPublished, creationHistory, user]); + + const filteredWorks = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + return works.filter(work => { + if (category !== 'all' && getCategoryFromWork(work) !== category) return false; + if (!query) return true; + const haystack = [ + work.title, + work.prompt, + work.negativePrompt, + work.publisherNickname, + work.params?.model, + work.params?.modelLabel, + work.type, + ].map(value => String(value || '').toLowerCase()).join('\n'); + return haystack.includes(query); + }); + }, [works, category, searchQuery]); + + const apiWorkIds = useMemo(() => new Set(apiWorks.map(work => work.id)), [apiWorks]); + + const handleCardImageLoad = useCallback((workId: string, e: React.SyntheticEvent) => { + const img = e.currentTarget; + if (img.naturalWidth <= 0 || img.naturalHeight <= 0) return; + + setMeasuredMediaSizes(prev => { + const current = prev[workId]; + if (current?.width === img.naturalWidth && current?.height === img.naturalHeight) { + return prev; + } + return { + ...prev, + [workId]: { width: img.naturalWidth, height: img.naturalHeight }, + }; + }); + }, []); + + const masonryColumns = useMemo(() => { + const columns = Array.from({ length: masonryColumnCount }, () => [] as GalleryWork[]); + const columnHeights = Array.from({ length: masonryColumnCount }, () => 0); + filteredWorks.forEach((work) => { + const targetIndex = columnHeights.indexOf(Math.min(...columnHeights)); + columns[targetIndex].push(work); + columnHeights[targetIndex] += getEstimatedWorkHeight(work, measuredMediaSizes[work.id]); + }); + return columns; + }, [filteredWorks, masonryColumnCount, measuredMediaSizes]); + + const selectedReferenceImages = useMemo( + () => selectedWork ? getWorkReferenceImages(selectedWork) : [], + [selectedWork], + ); + + const toggleLike = (id: string, e?: React.MouseEvent) => { + e?.stopPropagation(); + if (likedIds.has(id)) return; + setLikedIds(prev => new Set(prev).add(id)); + }; + + const handleDownload = async (url: string, filename: string, e?: React.MouseEvent) => { + e?.stopPropagation(); + const result = await downloadFile(url, filename); + if (!result.ok) { + window.open(url, '_blank'); + } + }; + + const toggleSelectGalleryWork = (id: string, e?: React.MouseEvent) => { + e?.stopPropagation(); + setSelectedGalleryIds(prev => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const handleDeleteGalleryWorks = async (ids: string[], e?: React.MouseEvent) => { + e?.stopPropagation(); + const targetIds = ids.filter(id => apiWorkIds.has(id)); + if (targetIds.length === 0) { + toast.error('没有可删除的服务器画廊作品'); + return; + } + const confirmed = window.confirm(targetIds.length === 1 ? '确认从画廊移除这个作品?' : `确认从画廊批量移除 ${targetIds.length} 个作品?`); + if (!confirmed) return; + + try { + const res = await fetch('/api/gallery', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({ ids: targetIds }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data.error || '删除失败'); + } + const removedIds = new Set((data.ids || targetIds) as string[]); + setApiWorks(prev => prev.filter(work => !removedIds.has(work.id))); + setSelectedGalleryIds(prev => new Set([...prev].filter(id => !removedIds.has(id)))); + if (selectedWork && removedIds.has(selectedWork.id)) { + setSelectedWork(null); + } + toast.success(`已从画廊移除 ${data.removed ?? removedIds.size} 个作品`); + } catch (err) { + toast.error(err instanceof Error ? err.message : '删除失败'); + } + }; + + return ( +
+
+ {/* Header */} +
+
+

作品画廊

+ {syncStatus === 'syncing' && ( + 同步本地数据... + )} +
+

探索社区创作,发现灵感之美

+
+ +
+ + setSearchQuery(event.target.value)} + placeholder="搜索作品、用户、提示词、模型" + className="h-8 min-w-0 flex-1 bg-transparent text-sm font-medium outline-none placeholder:text-muted-foreground/70" + /> + {searchQuery && ( + + )} +
+ + {/* Filters */} +
+
+ {CATEGORIES.map((cat) => { + const Icon = cat.icon; + return ( + + ); + })} +
+
+ + + {isAdmin && selectedGalleryIds.size > 0 && ( + + )} +
+
+ + {/* Gallery Grid */} + {loading ? ( +
+ +

加载中...

+
+ ) : filteredWorks.length === 0 ? ( +
+ +

暂无作品

+

创作并发布你的作品,让大家一起欣赏

+ +
+ ) : ( +
+ {masonryColumns.map((columnWorks, columnIndex) => ( +
+ {columnWorks.map((work) => ( + setSelectedWork(work)} + > +
+ {(work.thumbnailUrl || (work.url && !work.url.startsWith('data:'))) ? ( + {(work.prompt handleCardImageLoad(work.id, e)} + onDoubleClick={(e) => { e.stopPropagation(); setFullscreenSrc(work.url); }} + /> + ) : ( +
+ +
+ )} + {isAdmin && apiWorkIds.has(work.id) && ( + + )} + {(work.type === 'video' || work.type === 'text2video' || work.type === 'img2video') && ( + + 视频 + + )} + + {getCategoryLabel(work)} + + {/* Hover overlay */} +
+

+ {work.prompt} +

+
+ + + {isAdmin && apiWorkIds.has(work.id) && ( + + )} +
+
+
+ +
+
+
+ {work.publisherAvatarUrl ? ( + {work.publisherNickname} + ) : ( + getAvatarText(work.publisherNickname) + )} +
+ + {work.publisherNickname} + +
+
+ + {work.likes + (likedIds.has(work.id) ? 1 : 0)} +
+
+

+ {work.prompt} +

+
+
+ ))} +
+ ))} +
+ )} +
+ + {/* Detail - Fullscreen Overlay */} + {selectedWork && ( +
{ if (e.target === e.currentTarget) setSelectedWork(null); }} + > +
+ {selectedWork.url && !selectedWork.url.startsWith('data:') && ( + <> + +
+
+ + )} + {/* Left: Image/Video */} +
+ {selectedWork.type === 'video' || selectedWork.type === 'text2video' || selectedWork.type === 'img2video' ? ( +
+ + {/* Right: Info Panel */} +
+
+ {/* Close header */} +
+ +

作品详情

+ +
+ +
+ {/* Publisher info */} +
+
+ {selectedWork.publisherAvatarUrl ? ( + {selectedWork.publisherNickname} + ) : ( + getAvatarText(selectedWork.publisherNickname) + )} +
+
+

{selectedWork.publisherNickname}

+

+ + {formatDate(selectedWork.publishedAt)} +

+
+
+ + {/* Prompt */} + {(selectedWork.prompt || selectedWork.negativePrompt) && ( +
+ {selectedWork.prompt && ( +
+
+

+ + 提示词 +

+ +
+
+

{selectedWork.prompt}

+
+
+ )} + {selectedWork.negativePrompt && ( +
+
+

+ + 负面提示词 +

+ +
+
+

+ {selectedWork.negativePrompt} +

+
+
+ )} +
+ )} + + {/* Reference Image */} + {selectedReferenceImages.length > 0 && ( +
+
+
+ +

参考图

+
+ {selectedReferenceImages.length} 张 +
+
+ {selectedReferenceImages.map((url, index) => ( +
+ {`参考图 setFullscreenSrc(url)} + /> +
+ + +
+
+ ))} +
+
+ )} + +
+ {/* Model & Params */} + {selectedWork.params && Object.keys(selectedWork.params).length > 0 && ( +
+
+ +

模型与参数

+
+
+ {(!!selectedWork.params.modelLabel || !!selectedWork.params.model) && ( +
+

模型

+

{String(selectedWork.params.modelLabel || selectedWork.params.model || '')}

+
+ )} +
+

类型

+ {getCategoryLabel(selectedWork)} +
+ {!!selectedWork.params.size && ( +
+

尺寸

+

{String(selectedWork.params.size)}

+
+ )} + {!!selectedWork.params.steps && ( +
+

步数

+

{String(selectedWork.params.steps)}

+
+ )} + {!!selectedWork.params.cfg_scale && ( +
+

引导系数

+

{String(selectedWork.params.cfg_scale)}

+
+ )} + {!!selectedWork.params.seed && ( +
+

种子

+

{String(selectedWork.params.seed)}

+
+ )} +
+
+ )} + +
+ + + {isAdmin && apiWorkIds.has(selectedWork.id) && ( + + )} +
+
+
+
+
+
+ )} + + {/* Fullscreen image preview overlay */} + setFullscreenSrc(null)} + /> +
+ ); +} + + diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..e3b3d09 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,609 @@ +@import url('https://fonts.googleapis.cn/css2?family=Noto+Serif+SC:wght@200..900&display=swap'); +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-serif: "Noto Serif SC", "Songti SC", "SimSun", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --shadow-2xs: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.02); + --shadow-xs: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.02); + --shadow-sm: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.04), 0px 1px 2px -1px hsl(225 27.7778% 14.1176% / 0.04); + --shadow-md: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.04), 0px 2px 4px -1px hsl(225 27.7778% 14.1176% / 0.04); + --shadow-lg: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.04), 0px 4px 6px -1px hsl(225 27.7778% 14.1176% / 0.04); + --shadow-xl: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.04), 0px 8px 10px -1px hsl(225 27.7778% 14.1176% / 0.04); + --shadow-2xl: 0px 1px 2px 0px hsl(225 27.7778% 14.1176% / 0.10); +} + +:root { + --background: oklch(0.985 0.01 85); + --foreground: oklch(0.18 0.02 60); + --card: oklch(0.995 0.005 85); + --card-foreground: oklch(0.18 0.02 60); + --popover: oklch(0.995 0.005 85); + --popover-foreground: oklch(0.18 0.02 60); + --primary: oklch(0.62 0.17 55); + --primary-foreground: oklch(0.99 0.02 95); + --secondary: oklch(0.94 0.03 85); + --secondary-foreground: oklch(0.25 0.05 60); + --muted: oklch(0.92 0.025 80); + --muted-foreground: oklch(0.48 0.03 70); + --accent: oklch(0.90 0.06 90); + --accent-foreground: oklch(0.2 0.04 50); + --destructive: oklch(0.55 0.22 25); + --border: oklch(0.88 0.02 80); + --input: oklch(0.88 0.02 80); + --ring: oklch(0.62 0.17 55); + --chart-1: oklch(0.85 0.15 90); + --chart-2: oklch(0.75 0.16 70); + --chart-3: oklch(0.65 0.17 55); + --chart-4: oklch(0.55 0.15 45); + --chart-5: oklch(0.45 0.12 40); + --sidebar: oklch(0.96 0.015 85); + --sidebar-foreground: oklch(0.18 0.02 60); + --sidebar-primary: oklch(0.62 0.17 55); + --sidebar-primary-foreground: oklch(0.99 0.02 95); + --sidebar-accent: oklch(0.92 0.025 80); + --sidebar-accent-foreground: oklch(0.2 0.04 50); + --sidebar-border: oklch(0.88 0.02 80); + --sidebar-ring: oklch(0.62 0.17 55); + --radius: 0.8rem; +} + +.dark { + --background: oklch(0.16 0.015 260); + --foreground: oklch(0.98 0.01 80); + --card: oklch(0.21 0.025 260); + --card-foreground: oklch(0.98 0.01 80); + --popover: oklch(0.25 0.025 260); + --popover-foreground: oklch(0.98 0.01 80); + --primary: oklch(0.75 0.15 70); + --primary-foreground: oklch(0.2 0.05 50); + --secondary: oklch(0.26 0.03 260); + --secondary-foreground: oklch(0.98 0 0); + --muted: oklch(0.23 0.02 260); + --muted-foreground: oklch(0.65 0.02 260); + --accent: oklch(0.28 0.06 70); + --accent-foreground: oklch(0.98 0.02 80); + --destructive: oklch(0.58 0.18 25); + --border: oklch(1 0 0 / 12%); + --input: oklch(1 0 0 / 18%); + --ring: oklch(0.75 0.15 70); + --chart-1: oklch(0.85 0.15 90); + --chart-2: oklch(0.75 0.16 70); + --chart-3: oklch(0.65 0.17 55); + --chart-4: oklch(0.55 0.15 45); + --chart-5: oklch(0.45 0.12 40); + --sidebar: oklch(0.19 0.02 260); + --sidebar-foreground: oklch(0.98 0.01 80); + --sidebar-primary: oklch(0.75 0.15 70); + --sidebar-primary-foreground: oklch(0.2 0.05 50); + --sidebar-accent: oklch(0.24 0.025 260); + --sidebar-accent-foreground: oklch(0.98 0.01 80); + --sidebar-border: oklch(1 0 0 / 12%); + --sidebar-ring: oklch(0.75 0.15 70); +} + +@custom-variant dark (&:is(.dark *)); +@custom-variant light (&:is(.light *)); + +@layer base { + * { + @apply border-border outline-ring/50; + + } + body { + @apply bg-background text-foreground font-serif; + font-size: 17px; + line-height: 1.65; + + } + + .light body { + background: + linear-gradient(135deg, rgb(250 247 241) 0%, rgb(237 242 248) 42%, rgb(251 247 236) 100%), + var(--background); + } + + @media (max-width: 767px) { + body { + padding-bottom: calc(4.25rem + env(safe-area-inset-bottom)); + font-size: 16px; + line-height: 1.55; + } + } +} + +@layer components { + .liquid-surface { + position: relative; + overflow: hidden; + isolation: isolate; + border: 1px solid rgb(255 255 255 / 0.085); + background: + linear-gradient(180deg, rgb(18 25 38 / 0.78), rgb(7 11 19 / 0.72)), + rgb(8 13 22 / 0.78); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.07), + 0 12px 30px rgb(0 0 0 / 0.22); + backdrop-filter: blur(14px) saturate(112%); + -webkit-backdrop-filter: blur(14px) saturate(112%); + } + + .liquid-surface:hover { + border-color: rgb(255 255 255 / 0.085); + } + + .liquid-glass { + position: relative; + overflow: hidden; + isolation: isolate; + border: 1px solid rgb(255 255 255 / 0.12); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.075), rgb(255 255 255 / 0.026) 34%, rgb(2 6 23 / 0.38)), + rgb(10 16 27 / 0.70); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.12), + inset 0 0 0 1px rgb(255 255 255 / 0.025), + 0 16px 38px rgb(0 0 0 / 0.26); + backdrop-filter: blur(20px) saturate(122%); + -webkit-backdrop-filter: blur(20px) saturate(122%); + } + + .liquid-glass::before { + content: ""; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + border-radius: inherit; + background: + linear-gradient(110deg, rgb(255 255 255 / 0.11), transparent 24% 80%, rgb(244 166 36 / 0.035)), + linear-gradient(180deg, rgb(255 255 255 / 0.055), transparent 30%); + opacity: 0.46; + } + + .liquid-glass:hover { + border-color: rgb(255 255 255 / 0.12); + } + + .liquid-glass:hover::before { + opacity: 0.46; + } + + .liquid-glass:active { + filter: none; + } + + .liquid-glass-soft { + position: relative; + overflow: hidden; + isolation: isolate; + border: 1px solid rgb(255 255 255 / 0.085); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.044), rgb(255 255 255 / 0.012) 36%, rgb(2 6 23 / 0.26)), + rgb(8 13 22 / 0.66); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.075), + 0 10px 28px rgb(0 0 0 / 0.20); + backdrop-filter: blur(16px) saturate(116%); + -webkit-backdrop-filter: blur(16px) saturate(116%); + } + + .liquid-glass-soft:hover { + border-color: rgb(255 255 255 / 0.085); + } + + .liquid-glass-control { + position: relative; + overflow: hidden; + border: 1px solid rgb(255 255 255 / 0.105); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.065), rgb(255 255 255 / 0.02) 44%, rgb(2 6 23 / 0.26)), + rgb(15 23 42 / 0.64); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.09), + 0 8px 20px rgb(0 0 0 / 0.16); + backdrop-filter: blur(14px) saturate(118%); + -webkit-backdrop-filter: blur(14px) saturate(118%); + } + + .liquid-glass-control:hover { + border-color: rgb(255 255 255 / 0.105); + } + + .liquid-glass-control:active { + transform: none; + } + + .glass-panel { + position: relative; + overflow: hidden; + isolation: isolate; + border: 1px solid rgb(255 255 255 / 0.12); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.075), rgb(255 255 255 / 0.026) 34%, rgb(2 6 23 / 0.38)), + rgb(10 16 27 / 0.70); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.12), + inset 0 0 0 1px rgb(255 255 255 / 0.025), + 0 16px 38px rgb(0 0 0 / 0.26); + backdrop-filter: blur(20px) saturate(122%); + -webkit-backdrop-filter: blur(20px) saturate(122%); + } + + .glass-card { + position: relative; + overflow: hidden; + isolation: isolate; + border: 1px solid rgb(255 255 255 / 0.085); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.044), rgb(255 255 255 / 0.012) 36%, rgb(2 6 23 / 0.26)), + rgb(8 13 22 / 0.66); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.075), + 0 10px 28px rgb(0 0 0 / 0.20); + backdrop-filter: blur(16px) saturate(116%); + -webkit-backdrop-filter: blur(16px) saturate(116%); + } + + .glass-popover { + @apply border border-white/20 bg-popover/80 shadow-xl shadow-black/10 backdrop-blur-xl backdrop-saturate-150 dark:border-white/10 dark:bg-popover/68 dark:shadow-black/30; + } + + .light .liquid-surface, + .light .liquid-glass, + .light .glass-panel { + border-color: rgb(105 86 54 / 0.18); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.54), rgb(255 255 255 / 0.22) 46%, rgb(248 243 232 / 0.16)), + linear-gradient(135deg, rgb(255 255 255 / 0.20), rgb(213 226 241 / 0.12) 42%, rgb(238 204 126 / 0.10)), + rgb(255 255 255 / 0.28); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.78), + inset 0 -1px 0 rgb(103 82 45 / 0.08), + 0 18px 48px rgb(73 55 26 / 0.13), + 0 2px 10px rgb(255 255 255 / 0.34); + backdrop-filter: blur(26px) saturate(168%) contrast(104%); + -webkit-backdrop-filter: blur(26px) saturate(168%) contrast(104%); + } + + .light .liquid-glass-soft, + .light .liquid-glass-control, + .light .glass-card { + border-color: rgb(105 86 54 / 0.15); + background: + linear-gradient(180deg, rgb(255 255 255 / 0.46), rgb(255 255 255 / 0.18) 52%, rgb(246 240 229 / 0.14)), + linear-gradient(135deg, rgb(255 255 255 / 0.18), rgb(216 228 241 / 0.10) 44%, rgb(238 204 126 / 0.08)), + rgb(255 255 255 / 0.22); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.70), + inset 0 -1px 0 rgb(103 82 45 / 0.06), + 0 12px 34px rgb(73 55 26 / 0.10); + backdrop-filter: blur(22px) saturate(158%) contrast(103%); + -webkit-backdrop-filter: blur(22px) saturate(158%) contrast(103%); + } + + .light .liquid-glass::before, + .light .glass-panel::before, + .light .glass-card::before, + .light .liquid-glass-soft::before, + .light .liquid-glass-control::before, + .light .liquid-surface::before { + content: ""; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + border-radius: inherit; + background: + linear-gradient(115deg, rgb(255 255 255 / 0.50), transparent 24% 78%, rgb(214 162 62 / 0.10)), + linear-gradient(180deg, rgb(255 255 255 / 0.34), transparent 38%); + opacity: 0.56; + } + + .light .glass-popover { + border-color: rgb(105 86 54 / 0.16); + background: rgb(255 255 255 / 0.42); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.72), + 0 18px 44px rgb(73 55 26 / 0.12); + backdrop-filter: blur(24px) saturate(160%); + -webkit-backdrop-filter: blur(24px) saturate(160%); + } + + .light [data-slot="input"], + .light [data-slot="textarea"], + .light [data-slot="select-trigger"], + .light input, + .light textarea, + .light button[role="combobox"] { + border-color: rgb(116 88 43 / 0.44) !important; + background: + linear-gradient(180deg, rgb(255 255 255 / 0.72), rgb(255 255 255 / 0.42)), + rgb(255 255 255 / 0.46) !important; + box-shadow: + inset 0 0 0 1px rgb(116 88 43 / 0.16), + inset 0 1px 0 rgb(255 255 255 / 0.78), + 0 4px 14px rgb(92 69 32 / 0.07) !important; + } + + .light [data-slot="input"]:hover, + .light [data-slot="textarea"]:hover, + .light [data-slot="select-trigger"]:hover, + .light input:hover, + .light textarea:hover, + .light button[role="combobox"]:hover { + border-color: rgb(116 88 43 / 0.58) !important; + background: + linear-gradient(180deg, rgb(255 255 255 / 0.78), rgb(255 255 255 / 0.48)), + rgb(255 255 255 / 0.52) !important; + box-shadow: + inset 0 0 0 1px rgb(116 88 43 / 0.22), + inset 0 1px 0 rgb(255 255 255 / 0.82), + 0 5px 16px rgb(92 69 32 / 0.08) !important; + } + + .light [data-slot="input"]:focus-visible, + .light [data-slot="textarea"]:focus-visible, + .light [data-slot="select-trigger"]:focus-visible, + .light input:focus-visible, + .light textarea:focus-visible, + .light button[role="combobox"]:focus-visible { + border-color: rgb(196 126 30 / 0.86) !important; + box-shadow: + inset 0 0 0 2px rgb(244 166 36 / 0.30), + 0 0 0 1px rgb(244 166 36 / 0.24), + 0 8px 22px rgb(92 69 32 / 0.08) !important; + } + + .light .border-dashed { + border-color: rgb(116 88 43 / 0.42) !important; + box-shadow: + inset 0 0 0 1px rgb(116 88 43 / 0.10), + 0 10px 30px rgb(92 69 32 / 0.06); + } + + .light .border-dotted { + border-color: rgb(116 88 43 / 0.38) !important; + } + + @media (max-width: 767px) { + .mobile-page-shell { + @apply px-3 py-4; + } + + .mobile-stack { + @apply grid grid-cols-1 gap-4; + } + + .mobile-card-list { + @apply space-y-3; + } + } + + @media (prefers-reduced-motion: reduce) {} +} + +/* Markdown styles for announcements */ +.announcement-markdown { + font-size: 0.875rem; + line-height: 1.7; + color: var(--foreground); +} +.announcement-markdown h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 0.75rem 0 0.5rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--border); +} +.announcement-markdown h2 { + font-size: 1.25rem; + font-weight: 700; + margin: 0.75rem 0 0.4rem; +} +.announcement-markdown h3 { + font-size: 1.1rem; + font-weight: 600; + margin: 0.6rem 0 0.3rem; +} +.announcement-markdown h4, .announcement-markdown h5, .announcement-markdown h6 { + font-weight: 600; + margin: 0.5rem 0 0.25rem; +} +.announcement-markdown p { + margin: 0.4rem 0; +} +.announcement-markdown strong { + font-weight: 700; +} +.announcement-markdown em { + font-style: italic; +} +.announcement-markdown del { + text-decoration: line-through; + opacity: 0.6; +} +.announcement-markdown ul, .announcement-markdown ol { + margin: 0.4rem 0; + padding-left: 1.5rem; +} +.announcement-markdown ul { + list-style-type: disc; +} +.announcement-markdown ol { + list-style-type: decimal; +} +.announcement-markdown li { + margin: 0.15rem 0; +} +.announcement-markdown blockquote { + margin: 0.5rem 0; + padding: 0.5rem 0.75rem; + border-left: 3px solid var(--primary); + background: var(--muted); + border-radius: 0 0.375rem 0.375rem 0; +} +.announcement-markdown blockquote p { + margin: 0.15rem 0; +} +.announcement-markdown code { + font-size: 0.8rem; + padding: 0.15rem 0.35rem; + background: var(--muted); + border-radius: 0.25rem; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; +} +.announcement-markdown pre { + margin: 0.5rem 0; + padding: 0.75rem; + background: var(--muted); + border-radius: 0.375rem; + overflow-x: auto; +} +.announcement-markdown pre code { + padding: 0; + background: transparent; + font-size: 0.8rem; +} +.announcement-markdown a { + color: var(--primary); + text-decoration: underline; + text-underline-offset: 2px; +} +.announcement-markdown a:hover { + opacity: 0.8; +} +.announcement-markdown hr { + margin: 0.75rem 0; + border-color: var(--border); +} +.announcement-markdown table { + width: 100%; + margin: 0.5rem 0; + border-collapse: collapse; + font-size: 0.8rem; +} +.announcement-markdown th, .announcement-markdown td { + border: 1px solid var(--border); + padding: 0.4rem 0.6rem; + text-align: left; +} +.announcement-markdown th { + background: var(--muted); + font-weight: 600; +} +.announcement-markdown img { + max-width: 100%; + border-radius: 0.375rem; +} + +@keyframes golden-shimmer { + 0% { + transform: translateX(-18%) skewX(-10deg); + opacity: 0.35; + } + 45% { + opacity: 0.95; + } + 100% { + transform: translateX(18%) skewX(-10deg); + opacity: 0.42; + } +} + +@keyframes golden-sweep { + 0% { + transform: translateX(-34%) translateY(-50%) rotate(-8deg); + opacity: 0; + } + 18% { + opacity: 0.7; + } + 52% { + opacity: 1; + } + 100% { + transform: translateX(34%) translateY(-50%) rotate(-8deg); + opacity: 0; + } +} + +@keyframes golden-breathe { + 0%, 100% { + transform: translate(-50%, -50%) scale(0.86); + opacity: 0.36; + } + 50% { + transform: translate(-50%, -50%) scale(1.18); + opacity: 0.72; + } +} + +@keyframes golden-drift { + 0% { + transform: translateX(0) scale(0.95); + opacity: 0.22; + } + 45% { + opacity: 0.62; + } + 100% { + transform: translateX(240%) scale(1.08); + opacity: 0.2; + } +} + +@keyframes golden-float-random { + 0%, 100% { + transform: translate(-50%, -50%) translate3d(0, 0, 0) scale(0.88); + filter: blur(28px); + } + 42% { + transform: translate(-50%, -50%) translate3d(var(--float-x), var(--float-y), 0) scale(1.16); + filter: blur(38px); + } + 68% { + transform: translate(-50%, -50%) translate3d(calc(var(--float-x) * -0.35), calc(var(--float-y) * 0.42), 0) scale(1.02); + filter: blur(34px); + } +} diff --git a/src/app/help/page.tsx b/src/app/help/page.tsx new file mode 100644 index 0000000..0cb524c --- /dev/null +++ b/src/app/help/page.tsx @@ -0,0 +1,5 @@ +import { SitePolicyPage } from '@/components/site-policy-page'; + +export default function HelpPage() { + return ; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..6d0a987 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from 'next'; +import { Inspector } from 'react-dev-inspector'; +import { ThemeProvider } from 'next-themes'; +import { AppShell } from '@/modules/web'; +import './globals.css'; + +export const metadata: Metadata = { + title: { + default: '妙境 - AI创作平台', + template: '%s | 妙境', + }, + description: '妙手丹青,境随心造 - 一站式AI多模态创作平台,提供文生图、图生图、文生视频、图生视频四大核心能力', + icons: { + icon: '/favicon.png', + apple: '/apple-touch-icon.png', + }, + keywords: [ + '妙境', + 'AI创作', + '文生图', + '图生图', + '文生视频', + '图生视频', + 'AI绘画', + 'AI视频', + ], +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const isDev = process.env.COZE_PROJECT_ENV === 'DEV'; + + return ( + + + + {isDev && } + {children} + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..b82a8dd --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,293 @@ +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { AnnouncementPopup } from '@/components/announcement-popup'; +import { BillingPlanGuard } from '@/components/billing-plan-guard'; +import { SiteName, SiteLogo } from '@/components/site-brand'; +import { SiteFooter } from '@/components/site-footer'; +import { + Brush, + ImagePlus, + Video, + Film, + ArrowRight, + Zap, + Shield, + Coins, + Layers, + Check, + Sparkles, +} from 'lucide-react'; + +const features = [ + { + icon: Brush, + title: '文生图', + desc: '用文字描述你的想象,AI即刻生成精美画作。支持多种风格、尺寸与参数调优。', + href: '/create?type=text2img', + gradient: 'from-amber-500/20 to-orange-500/10', + }, + { + icon: ImagePlus, + title: '图生图', + desc: '上传参考图片,AI基于你的素材进行风格迁移、场景变换和创意延展。', + href: '/create?type=img2img', + gradient: 'from-emerald-500/20 to-teal-500/10', + }, + { + icon: Video, + title: '文生视频', + desc: '输入场景描述,AI生成流畅的动态视频。支持多种镜头语言和风格设定。', + href: '/create?type=text2video', + gradient: 'from-rose-500/20 to-pink-500/10', + }, + { + icon: Film, + title: '图生视频', + desc: '将静态图片转化为动态视频,照片动画化、产品展示、场景延续一站搞定。', + href: '/create?type=img2video', + gradient: 'from-sky-500/20 to-cyan-500/10', + }, +]; + +const highlights = [ + { icon: Zap, title: '极速创作', desc: '数秒出图,分钟出视频,AI辅助将传统流程缩短90%' }, + { icon: Shield, title: '数据安全', desc: '多租户数据隔离,企业级安全标准,创作内容私密保护' }, + { icon: Coins, title: '灵活计费', desc: '积分制+订阅制双模式,按需付费,用多少花多少' }, + { icon: Layers, title: '多模型支持', desc: '兼容主流AI模型,支持自备API,灵活切换无锁定' }, +]; + +const pricing = [ + { + tier: '免费版', + price: '0', + desc: '体验核心创作能力', + features: ['每日5次创作额度', '标准画质输出', '社区作品展示', '基础参数调整'], + cta: '免费开始', + popular: false, + }, + { + tier: '基础版', + price: '29', + desc: '适合轻度创作者', + features: ['每日50次创作额度', '高清画质输出', '私有作品存储', '全部参数解锁', '作品批量下载'], + cta: '立即订阅', + popular: false, + }, + { + tier: '专业版', + price: '99', + desc: '适合专业创作者与团队', + features: ['无限创作额度', '4K超清输出', '自定义API接入', '批量处理能力', '优先处理队列', '高级风格预设'], + cta: '升级专业版', + popular: true, + }, + { + tier: '企业版', + price: '499', + desc: '适合企业与大型团队', + features: ['无限创作+团队协作', '专属API额度', '品牌风格定制', '私有化部署选项', '7x24技术支持', '商业版权保障'], + cta: '联系销售', + popular: false, + }, +]; + +export default function HomePage() { + return ( +
+ {/* Announcement Popup */} + + + {/* Hero Section */} +
+ {/* Background decoration */} +
+
+
+
+ +
+ + + 一站式AI多模态创作平台 + + +

+ 妙手丹青 + 境随心造 +

+ +

+ 用AI释放你的创造力。文生图、图生图、文生视频、图生视频 — + 四大核心能力,从想象到作品只需一步。 +

+ +
+ + + + + + +
+ + {/* Stats */} +
+ {[ + { value: '4', label: '核心创作能力' }, + { value: '10s', label: '平均出图时间' }, + { value: '100+', label: '预设风格' }, + { value: '99.9%', label: '服务可用性' }, + ].map((stat) => ( +
+
{stat.value}
+
{stat.label}
+
+ ))} +
+
+
+ + {/* Core Features */} +
+
+
+

四大核心能力

+

从文字到画面,从静态到动态,全方位AI创作体验

+
+ +
+ {features.map((feat) => { + const Icon = feat.icon; + return ( + + + +
+ +
+

+ {feat.title} +

+

+ {feat.desc} +

+
+ 立即体验 +
+
+
+ + ); + })} +
+
+
+ + {/* Highlights */} +
+
+
+

为什么选择妙境

+

创作无界,效率无限

+
+ +
+ {highlights.map((item) => { + const Icon = item.icon; + return ( +
+
+ +
+

{item.title}

+

{item.desc}

+
+ ); + })} +
+
+
+ + {/* Pricing */} + +
+
+
+

灵活的计费方案

+

按需选择,从免费体验到企业定制

+
+ +
+ {pricing.map((plan) => ( + + {plan.popular && ( +
+ 最受欢迎 +
+ )} + +

{plan.tier}

+
+ ¥{plan.price} + /月 +
+

{plan.desc}

+
    + {plan.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+ + + +
+
+ ))} +
+
+
+
+ + {/* CTA */} +
+
+

准备好了吗?

+

+ 加入数千名创作者,用AI开启你的创作之旅 +

+
+ + + +
+
+
+ + +
+ ); +} diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx new file mode 100644 index 0000000..f21adba --- /dev/null +++ b/src/app/privacy/page.tsx @@ -0,0 +1,5 @@ +import { SitePolicyPage } from '@/components/site-policy-page'; + +export default function PrivacyPage() { + return ; +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..64faf15 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,767 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import dynamic from 'next/dynamic'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type { ManagedModelConfigResponse, ManagedModelRecommendation, ManagedModelType } from '@/lib/model-config-types'; +import { useCustomApiKeys } from '@/lib/custom-api-store'; +import { useCreationHistory, type CreationRecord, isPlaceholder } from '@/lib/creation-history-store'; +import { useCreditRecords, formatRecordTime } from '@/lib/credit-records-store'; +import { useUserOrders, formatOrderTime } from '@/lib/order-store'; +import { useAuth } from '@/lib/auth-store'; +import { useSiteConfig } from '@/lib/site-config'; +import { CreationDetailDialog } from '@/components/creation-detail-dialog'; +import { + User, + CreditCard, + Crown, + Receipt, + Image, + Key, + Coins, + Calendar, + Shield, + TrendingUp, + Gift, + Zap, + Settings, + Globe, + Cpu, + Trash2, + Eye, + EyeOff, + Plus, + Check, + Loader2, + Film, + LogOut, + LogIn, + ExternalLink, + Sparkles, + MessageSquare, + ImageOff, + Camera, + MailCheck, +} from 'lucide-react'; + +const EMAIL_REGEX = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/; + +function isEmail(value: string) { + return EMAIL_REGEX.test(value.trim()); +} + +function sanitizeCode(value: string) { + return value.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 10); +} + +const ApiKeyManager = dynamic(() => import('@/components/profile/api-key-manager'), { ssr: false }); +const CreationHistoryTab = dynamic(() => import('@/components/profile/creation-history-tab'), { ssr: false }); +const CreditsTab = dynamic(() => import('@/components/profile/credits-tab'), { ssr: false }); +const OrdersTab = dynamic(() => import('@/components/profile/orders-tab'), { ssr: false }); +const membershipTiers = [ + { tier: 'free', name: '免费版', price: 0, dailyQuota: 5, features: ['每日5次创作', '标准画质', '社区展示'] }, + { tier: 'pro', name: 'Pro版', price: 29, dailyQuota: 50, features: ['每日50次创作', '高清画质', '私有存储', '批量下载'] }, + { tier: 'max', name: 'Max版', price: 99, dailyQuota: -1, features: ['无限创作', '4K超清', '自定义API', '批量处理', '优先队列'] }, + { tier: 'ultra', name: 'Ultra版', price: 499, dailyQuota: -1, features: ['团队协作', '专属额度', '品牌定制', '私有部署', '7x24支持'] }, +]; + +const membershipRank: Record = { + free: 0, + basic: 1, + pro: 1, + max: 2, + enterprise: 3, + ultra: 3, +}; + +function normalizeMembershipTier(tier?: string | null) { + if (tier === 'basic') return 'pro'; + if (tier === 'enterprise') return 'ultra'; + return tier || 'free'; +} + +export default function ProfilePage() { + const { isLoggedIn, user, accessToken, logout, isAdmin, isVip, refreshProfile, updateProfile } = useAuth(); + const { config: siteConfig } = useSiteConfig(); + const router = useRouter(); + const [activeTab, setActiveTab] = useState('account'); + const [mounted, setMounted] = useState(false); + const [accountForm, setAccountForm] = useState({ nickname: '', email: '', phone: '', avatarUrl: '' }); + const [passwordForm, setPasswordForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); + const [savingAccount, setSavingAccount] = useState(false); + const [processingAvatar, setProcessingAvatar] = useState(false); + const [accountMessage, setAccountMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [showEmailVerify, setShowEmailVerify] = useState(false); + const [emailVerifyCode, setEmailVerifyCode] = useState(''); + const [emailVerifyCooldown, setEmailVerifyCooldown] = useState(0); + const [sendingEmailCode, setSendingEmailCode] = useState(false); + const [verifyingEmail, setVerifyingEmail] = useState(false); + const { records: creationRecords } = useCreationHistory(); + const { records: creditRecords } = useCreditRecords(); + const { orders } = useUserOrders(); + const membershipEnabled = siteConfig.membershipEnabled !== false; + + useEffect(() => { + setMounted(true); + }, []); + + // Refresh profile from server on mount to pick up admin changes + useEffect(() => { + if (isLoggedIn) { + refreshProfile(); + } + }, [isLoggedIn, refreshProfile]); + + useEffect(() => { + if (!membershipEnabled && ['membership', 'credits', 'orders'].includes(activeTab)) { + setActiveTab('account'); + } + }, [membershipEnabled, activeTab]); + + useEffect(() => { + if (!user) return; + setAccountForm({ + nickname: user.nickname || '', + email: user.email || '', + phone: user.phone || '', + avatarUrl: user.avatarUrl || '', + }); + }, [user?.id, user?.nickname, user?.email, user?.phone, user?.avatarUrl]); + + useEffect(() => { + if (emailVerifyCooldown <= 0) return; + const timer = window.setInterval(() => setEmailVerifyCooldown(prev => Math.max(0, prev - 1)), 1000); + return () => window.clearInterval(timer); + }, [emailVerifyCooldown]); + + // Use auth store data directly + const profile = { + nickname: user?.nickname || '游客', + email: user?.email || '', + phone: user?.phone || '', + role: user?.role || 'user', + membership_tier: user?.membershipTier || 'free', + credits_balance: user?.creditsBalance ?? 0, + daily_quota_used: user?.dailyQuotaUsed ?? 0, + daily_quota_limit: user?.dailyQuotaLimit ?? 5, + avatar_url: user?.avatarUrl || '', + created_at: user?.createdAt || '', + email_verified: user?.emailVerified === true, + email_verified_at: user?.emailVerifiedAt || '', + }; + + const normalizedMembershipTier = normalizeMembershipTier(profile.membership_tier); + const currentMembershipRank = membershipRank[normalizedMembershipTier] ?? 0; + const tierInfo = membershipTiers.find(t => t.tier === normalizedMembershipTier) || membershipTiers[0]; + + // Role display info + const roleInfo: Record = { + admin: { label: '管理员', color: 'text-primary' }, + enterprise_admin: { label: '企业管理员', color: 'text-primary' }, + vip: { label: 'VIP', color: 'text-primary' }, + user: { label: '普通用户', color: 'text-muted-foreground' }, + }; + const currentRole = roleInfo[profile.role] || roleInfo.user; + + const handleLogout = () => { + logout(); + router.push('/'); + }; + + const handleAvatarChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ''; + if (!file) return; + + if (!file.type.startsWith('image/')) { + setAccountMessage({ type: 'error', text: '请选择图片文件作为头像' }); + return; + } + + if (file.size > 5 * 1024 * 1024) { + setAccountMessage({ type: 'error', text: '头像图片不能超过 5MB' }); + return; + } + + setProcessingAvatar(true); + setAccountMessage(null); + + const reader = new FileReader(); + reader.onload = () => { + const image = new window.Image(); + image.onload = () => { + const size = 512; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + setProcessingAvatar(false); + setAccountMessage({ type: 'error', text: '头像处理失败,请换一张图片' }); + return; + } + + const side = Math.min(image.width, image.height); + const sx = (image.width - side) / 2; + const sy = (image.height - side) / 2; + ctx.drawImage(image, sx, sy, side, side, 0, 0, size, size); + const avatarUrl = canvas.toDataURL('image/jpeg', 0.86); + setAccountForm(prev => ({ ...prev, avatarUrl })); + setProcessingAvatar(false); + }; + image.onerror = () => { + setProcessingAvatar(false); + setAccountMessage({ type: 'error', text: '头像读取失败,请换一张图片' }); + }; + image.src = String(reader.result || ''); + }; + reader.onerror = () => { + setProcessingAvatar(false); + setAccountMessage({ type: 'error', text: '头像读取失败,请换一张图片' }); + }; + reader.readAsDataURL(file); + }; + + const handleAccountSave = async () => { + if (!user || !accessToken) { + setAccountMessage({ type: 'error', text: '请先登录后再修改资料' }); + return; + } + + if (passwordForm.newPassword || passwordForm.confirmPassword || passwordForm.currentPassword) { + if (passwordForm.newPassword.length < 6) { + setAccountMessage({ type: 'error', text: '新密码至少需要 6 位' }); + return; + } + if (passwordForm.newPassword !== passwordForm.confirmPassword) { + setAccountMessage({ type: 'error', text: '两次输入的新密码不一致' }); + return; + } + } + + setSavingAccount(true); + setAccountMessage(null); + + try { + const payload: Record = { + email: accountForm.email, + nickname: accountForm.nickname, + phone: accountForm.phone, + avatarUrl: accountForm.avatarUrl, + }; + + if (passwordForm.newPassword) { + payload.currentPassword = passwordForm.currentPassword; + payload.newPassword = passwordForm.newPassword; + } + + const response = await fetch('/api/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(payload), + }); + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data.error || '保存失败'); + } + + if (data.profile) { + updateProfile({ + email: data.profile.email, + nickname: data.profile.nickname, + phone: data.profile.phone || null, + membershipTier: data.profile.membership_tier || user.membershipTier, + creditsBalance: data.profile.credits_balance ?? user.creditsBalance, + dailyQuotaUsed: data.profile.daily_quota_used ?? user.dailyQuotaUsed, + dailyQuotaLimit: data.profile.daily_quota_limit ?? user.dailyQuotaLimit, + avatarUrl: data.profile.avatar_url ?? user.avatarUrl, + createdAt: data.profile.created_at ?? user.createdAt, + emailVerified: data.profile.email_verified === true, + emailVerifiedAt: data.profile.email_verified_at ?? null, + }); + } + + setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' }); + setAccountMessage({ type: 'success', text: '账号资料已保存' }); + refreshProfile(); + } catch (error) { + setAccountMessage({ type: 'error', text: error instanceof Error ? error.message : '保存失败' }); + } finally { + setSavingAccount(false); + } + }; + + const handleSendProfileEmailCode = async () => { + if (!accessToken) { + setAccountMessage({ type: 'error', text: '请先登录后再验证邮箱' }); + return; + } + if (!isEmail(accountForm.email)) { + setAccountMessage({ type: 'error', text: '请输入正确的邮箱地址' }); + return; + } + setSendingEmailCode(true); + setAccountMessage(null); + try { + const response = await fetch('/api/email/send-profile-code', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ email: accountForm.email }), + }); + const data = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(data.error || '验证码发送失败'); + setEmailVerifyCooldown(data.cooldown || 60); + setShowEmailVerify(true); + setAccountMessage({ type: 'success', text: data.message || '验证码已发送,请查收邮箱' }); + } catch (error) { + setAccountMessage({ type: 'error', text: error instanceof Error ? error.message : '验证码发送失败' }); + } finally { + setSendingEmailCode(false); + } + }; + + const handleVerifyProfileEmail = async () => { + if (!accessToken) return; + if (!isEmail(accountForm.email) || !emailVerifyCode) { + setAccountMessage({ type: 'error', text: '请填写邮箱和验证码' }); + return; + } + setVerifyingEmail(true); + try { + const response = await fetch('/api/email/verify-profile', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ email: accountForm.email, code: emailVerifyCode }), + }); + const data = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(data.error || '邮箱验证失败'); + if (data.profile) { + updateProfile({ + email: data.profile.email, + nickname: data.profile.nickname, + phone: data.profile.phone || null, + membershipTier: data.profile.membership_tier || user?.membershipTier || 'free', + creditsBalance: data.profile.credits_balance ?? user?.creditsBalance ?? 0, + dailyQuotaUsed: data.profile.daily_quota_used ?? user?.dailyQuotaUsed ?? 0, + dailyQuotaLimit: data.profile.daily_quota_limit ?? user?.dailyQuotaLimit ?? 5, + avatarUrl: data.profile.avatar_url ?? user?.avatarUrl ?? null, + createdAt: data.profile.created_at ?? user?.createdAt ?? null, + emailVerified: data.profile.email_verified === true, + emailVerifiedAt: data.profile.email_verified_at ?? null, + }); + } + setShowEmailVerify(false); + setEmailVerifyCode(''); + setAccountMessage({ type: 'success', text: data.message || '邮箱验证成功' }); + refreshProfile(); + } catch (error) { + setAccountMessage({ type: 'error', text: error instanceof Error ? error.message : '邮箱验证失败' }); + } finally { + setVerifyingEmail(false); + } + }; + + // Not logged in (after hydration) - show login prompt + if (mounted && !isLoggedIn) { + return ( +
+ + +
+ +
+

尚未登录

+

登录后可以管理你的创作、积分和 API 密钥

+ +
+
+
+ ); + } + + // Before hydration - render placeholder to avoid SSR/client mismatch + if (!mounted) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+ {/* Profile Header */} +
+
+
+
+ {profile.avatar_url ? ( + {profile.nickname} + ) : ( + profile.nickname[0] + )} +
+
+

{profile.nickname}

+
+ + {profile.role === 'admin' && } + {currentRole.label} + + {membershipEnabled && {tierInfo.name}} + + {profile.email_verified ? '邮箱已验证' : '邮箱未验证'} + + {profile.email} +
+
+
+ +
+ + {/* Quick Stats */} +
+ {membershipEnabled && ( + + +
+ +
+
+

{profile.credits_balance}

+

剩余积分

+
+
+
+ )} + {membershipEnabled && ( + + +
+ +
+
+

{profile.daily_quota_used}/{profile.daily_quota_limit}

+

今日额度

+
+
+
+ )} + + +
+ +
+
+

{creationRecords.length}

+

创作记录

+
+
+
+ {membershipEnabled && ( + + +
+ +
+
+

{tierInfo.name}

+

当前会员

+
+
+
+ )} +
+
+ + {/* Tabs */} + + + 账户 + {membershipEnabled && 会员} + {membershipEnabled && 积分} + {membershipEnabled && 订单} + 历史 + API + + + {/* Account Tab */} + + + + 账户信息 + 管理你的账户基本信息 + + + {accountMessage && ( +
+ {accountMessage.text} +
+ )} + +
+
+ {accountForm.avatarUrl ? ( + 头像预览 + ) : ( + (accountForm.nickname || profile.nickname || '用').slice(0, 1).toUpperCase() + )} + {processingAvatar && ( +
+ +
+ )} +
+
+ +
+ + {accountForm.avatarUrl && ( + + )} +
+

支持 JPG、PNG、WebP,系统会自动裁剪为方形头像。

+
+
+ +
+
+ + setAccountForm(prev => ({ ...prev, nickname: event.target.value }))} + /> +
+
+ +
+ setAccountForm(prev => ({ ...prev, email: event.target.value }))} + /> + +
+

+ {profile.email_verified && accountForm.email === profile.email ? '该邮箱已验证,可用于找回密码。' : '邮箱验证后可用于找回密码和安全通知。'} +

+
+
+ + setAccountForm(prev => ({ ...prev, phone: event.target.value }))} + /> +
+
+ + +
+
+ + + +
+

安全设置

+
+
+ + setPasswordForm(prev => ({ ...prev, currentPassword: event.target.value }))} + autoComplete="current-password" + /> +
+
+ + setPasswordForm(prev => ({ ...prev, newPassword: event.target.value }))} + autoComplete="new-password" + /> +
+
+ + setPasswordForm(prev => ({ ...prev, confirmPassword: event.target.value }))} + autoComplete="new-password" + /> +
+
+
+ +
+ +
+
+
+
+ + {/* Membership Tab */} + {membershipEnabled && +
+ + + 会员订阅 + 升级会员享受更多创作权益 + + +
+ {membershipTiers.map((tier) => { + const tierRank = membershipRank[tier.tier] ?? 0; + const isCurrentTier = tier.tier === normalizedMembershipTier; + const isUnavailableTier = tierRank <= currentMembershipRank; + return ( + + +
+

{tier.name}

+ {isCurrentTier && ( + 当前 + )} +
+
+ ¥{tier.price} + /月 +
+
    + {tier.features.map((f) => ( +
  • +
    + {f} +
  • + ))} +
+ +
+
+ ); + })} +
+
+
} + + {/* Credits Tab */} + {membershipEnabled && + + } + + {/* Orders Tab */} + {membershipEnabled && + + } + + {/* Works Tab */} + + + + + {/* API Tab */} + + + +
+ + + + 验证邮箱 + 验证码已发送至 {accountForm.email},请在有效期内完成验证。 + +
+ + setEmailVerifyCode(sanitizeCode(event.target.value))} + className="uppercase" + maxLength={10} + /> +
+
+ + +
+
+
+
+
+ ); +} diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..f69940a --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,11 @@ +import { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/_next/', '/static/'], + }, + }; +} diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx new file mode 100644 index 0000000..4cd24fb --- /dev/null +++ b/src/app/terms/page.tsx @@ -0,0 +1,5 @@ +import { SitePolicyPage } from '@/components/site-policy-page'; + +export default function TermsPage() { + return ; +} diff --git a/src/components/account-theme-sync.tsx b/src/components/account-theme-sync.tsx new file mode 100644 index 0000000..17c6711 --- /dev/null +++ b/src/components/account-theme-sync.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import { useAuth } from '@/lib/auth-store'; + +export function AccountThemeSync() { + const { isLoggedIn, user } = useAuth(); + const { theme, setTheme } = useTheme(); + + useEffect(() => { + if (!isLoggedIn || !user?.preferredTheme) return; + if (theme !== user.preferredTheme) { + setTheme(user.preferredTheme); + } + }, [isLoggedIn, user?.preferredTheme, theme, setTheme]); + + return null; +} diff --git a/src/components/admin/announcement-tab.tsx b/src/components/admin/announcement-tab.tsx new file mode 100644 index 0000000..243bf4b --- /dev/null +++ b/src/components/admin/announcement-tab.tsx @@ -0,0 +1,388 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useAuth } from '@/lib/auth-store'; +import { useAdminConfig, type CreditPricing } from '@/lib/admin-store'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { useSiteConfig } from '@/lib/site-config'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { AlertCircle, AlertTriangle, Calendar, Check, CheckCircle2, Coins, CreditCard, Crown, Database, Download, Edit3, FileUp, Gift, Globe, Loader2, Megaphone, Pencil, Plus, Receipt, Save, ToggleLeft, Trash2, Upload, X, Zap } from 'lucide-react'; +import { toast } from 'sonner'; +// ============================================================ +// Tab 5: Announcement Management +// ============================================================ + +interface ServerAnnouncement { + id: string; + title: string; + content: string; + start_date?: string | null; + end_date?: string | null; + starts_at?: string | null; + expires_at?: string | null; + enabled?: boolean; + is_active?: boolean; + created_at: string; +} + +function announcementStartDate(ann: ServerAnnouncement): string { + return ann.start_date || ann.starts_at || ''; +} + +function announcementEndDate(ann: ServerAnnouncement): string { + return ann.end_date || ann.expires_at || ''; +} + +function announcementEnabled(ann: ServerAnnouncement): boolean { + return ann.enabled !== false && ann.is_active !== false; +} + +function formatDateInputValue(value: string): string { + if (!value) return ''; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ''; + const offset = date.getTimezoneOffset() * 60000; + return new Date(date.getTime() - offset).toISOString().slice(0, 16); +} + +export default function AnnouncementTab() { + const { accessToken } = useAuth(); + const [announcements, setAnnouncements] = useState([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + + // Form state + const [formTitle, setFormTitle] = useState(''); + const [formContent, setFormContent] = useState(''); + const [formStartDate, setFormStartDate] = useState(''); + const [formEndDate, setFormEndDate] = useState(''); + const [formEnabled, setFormEnabled] = useState(true); + const [previewMode, setPreviewMode] = useState(false); + const [now, setNow] = useState(0); + const [saving, setSaving] = useState(false); + + useEffect(() => { setNow(Date.now()); }, []); + + // Fetch announcements from server API + const fetchAnnouncements = useCallback(async () => { + try { + const res = await fetch('/api/announcements'); + if (res.ok) { + const data = await res.json(); + setAnnouncements(data || []); + } + } catch (err) { + console.error('[AnnouncementTab] fetch error:', err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { fetchAnnouncements(); }, [fetchAnnouncements]); + + const resetForm = () => { + setFormTitle(''); + setFormContent(''); + setFormStartDate(''); + setFormEndDate(''); + setFormEnabled(true); + setEditingId(null); + setShowForm(false); + }; + + const handleEdit = (ann: ServerAnnouncement) => { + setEditingId(ann.id); + setFormTitle(ann.title); + setFormContent(ann.content); + setFormStartDate(formatDateInputValue(announcementStartDate(ann))); + setFormEndDate(formatDateInputValue(announcementEndDate(ann))); + setFormEnabled(announcementEnabled(ann)); + setShowForm(true); + }; + + const handleSave = async () => { + if (!formTitle.trim() || !formContent.trim()) { + toast.error('请填写公告标题和内容'); + return; + } + if (!formStartDate || !formEndDate) { + toast.error('请设置有效期'); + return; + } + + setSaving(true); + try { + const body = { + title: formTitle.trim(), + content: formContent.trim(), + startDate: new Date(formStartDate).toISOString(), + endDate: new Date(formEndDate).toISOString(), + enabled: formEnabled, + }; + + let res: Response; + if (editingId) { + res = await fetch('/api/announcements', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({ id: editingId, ...body }), + }); + } else { + res = await fetch('/api/announcements', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify(body), + }); + } + + if (res.ok) { + toast.success(editingId ? '公告已更新' : '公告已创建'); + resetForm(); + fetchAnnouncements(); + } else { + const err = await res.json(); + toast.error(err.error || '操作失败'); + } + } catch { + toast.error('网络错误,请重试'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + try { + const res = await fetch(`/api/announcements?id=${encodeURIComponent(id)}`, { + method: 'DELETE', + headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined, + }); + if (res.ok) { + toast.success('公告已删除'); + fetchAnnouncements(); + } else { + toast.error('删除失败'); + } + } catch { + toast.error('网络错误,请重试'); + } + }; + + const handleToggleEnabled = async (id: string, enabled: boolean) => { + try { + const res = await fetch('/api/announcements', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({ id, enabled }), + }); + if (res.ok) { + fetchAnnouncements(); + } + } catch { + toast.error('操作失败'); + } + }; + + const isActive = (ann: ServerAnnouncement) => { + if (!announcementEnabled(ann)) return false; + if (!now) return false; + const startValue = announcementStartDate(ann); + const endValue = announcementEndDate(ann); + const start = startValue ? new Date(startValue).getTime() : 0; + const endDate = endValue ? new Date(endValue) : null; + if (endDate) endDate.setHours(23, 59, 59, 999); + const end = endDate ? endDate.getTime() : Number.POSITIVE_INFINITY; + if (Number.isNaN(start) || Number.isNaN(end)) return false; + return now >= start && now <= end; + }; + + const getStatusBadge = (ann: ServerAnnouncement) => { + if (isActive(ann)) return 生效中; + if (!announcementEnabled(ann)) return 已禁用; + const startValue = announcementStartDate(ann); + if (now && startValue && now < new Date(startValue).getTime()) return 待生效; + return 已过期; + }; + + if (loading) { + return ( + + + 加载中... + + + ); + } + + return ( +
+ + +
+
+ + + 公告管理 + + 创建和管理首页弹窗公告,可设置有效期,所有访客可见 +
+ +
+
+ + {announcements.length === 0 ? ( +
+ +

暂无公告

+
+ ) : ( + announcements.map(ann => ( +
+
+ +
+
+
+ {ann.title} + {getStatusBadge(ann)} +
+

{ann.content}

+
+ + + {announcementStartDate(ann) ? new Date(announcementStartDate(ann)).toLocaleDateString('zh-CN') : '立即'} - {announcementEndDate(ann) ? new Date(announcementEndDate(ann)).toLocaleDateString('zh-CN') : '长期'} + +
+
+
+ handleToggleEnabled(ann.id, checked)} + /> + + +
+
+ )) + )} +
+
+ + {/* Create / Edit Form */} + {showForm && ( + + + {editingId ? '编辑公告' : '新建公告'} + 设置公告标题、内容和有效期 + + +
+ + setFormTitle(e.target.value)} + /> +
+
+
+ +
+ + +
+
+ {previewMode ? ( +
+ {formContent ? ( + {formContent} + ) : ( +

暂无内容,请先在编辑模式输入

+ )} +
+ ) : ( +