5 Commits

Author SHA1 Message Date
c6e1d94510 fix: 修复 CI 时区测试失败 + 依赖安全升级 + 新增依赖检查 workflow
All checks were successful
CI / check (pull_request) Successful in 4m11s
Dependency Check / check (pull_request) Successful in 2m30s
- fix(server): getCourseClock 固定 UTC+8(Asia/Shanghai),与 attendance-settlement 保持一致,
  修复 CI(UTC 容器)下 attendance.lesson-session.spec.ts 失败
- chore(deps): @ant-design/x 2.9.0 / @ant-design/x-markdown 2.9.0 / mermaid 11.16.1;
  直接依赖 dompurify 3.4.13(修复生产依赖 audit 漏洞,mermaid/x-markdown 复用同一份)
- ci: 新增 dependency-check workflow(生产依赖 moderate+ / 全局 critical 失败,outdated 报告)
2026-08-10 10:05:05 +08:00
8c437cd082 Merge pull request 'fix: 迁移读取根 .env + 部署脚本本地直跑 + 健康检查读 PORT' (#66) from fix/datasource-root-env into main 2026-08-10 01:45:19 +00:00
4c95de2e56 Merge pull request 'fix(admin): 修复首次进入系统后点击任意导航被拉回 dashboard' (#67) from fix/route-redirect-keepalive into main 2026-08-10 01:44:02 +00:00
9d74bd2d51 ci(deploy): deploy.sh 支持本地直跑模式 + workflow 健康检查读取 .env PORT
Some checks failed
CI / check (pull_request) Failing after 5m3s
- deploy.sh 默认本地直跑(./deploy.sh 或 ./deploy.sh local),不再需要 SSH;传 ssh_host 才走远程版
- workflow 健康检查从 .env 读取 PORT(默认 3000),适配 PORT=3002 等自定义端口
2026-08-09 22:04:21 +08:00
f4072ce93a fix(db): 迁移 CLI 默认读取仓库根目录 .env
- datasource.ts 优先加载根目录 .env,再回退 apps/server/.env
- 生产只需维护根目录一份 .env,迁移脚本不再报 root 空密码
2026-08-09 22:02:00 +08:00
8 changed files with 246 additions and 154 deletions

View File

@@ -0,0 +1,44 @@
name: Dependency Check
# 依赖安全检查:生产依赖 moderate+ 或全局 critical 漏洞即失败;
# npm outdated 作为信息输出,不阻断。
# 注意npmmirror 未实现 audit 接口audit 必须显式指定官方 registry。
env:
NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
NPM_CONFIG_AUDIT: "false"
NPM_CONFIG_FUND: "false"
NPM_CONFIG_REPLACE_REGISTRY_HOST: always
CI: "true"
on:
pull_request:
branches: [main]
schedule:
- cron: '30 2 * * 1' # 每周一 02:30 复查已发布的新漏洞
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
container: node:22.22.0-bookworm
timeout-minutes: 20
steps:
- name: Checkout
uses: https://gitee.com/mirrors_actions/checkout@v4
with:
github-server-url: https://git.gongxue100.com
fetch-depth: 1
- name: Install dependencies
run: npm ci
- name: Audit production dependencies (moderate+)
run: npm audit --registry=https://registry.npmjs.org --omit=dev --audit-level=moderate
- name: Audit all dependencies (critical only)
run: npm audit --registry=https://registry.npmjs.org --audit-level=critical
- name: Outdated report (informational)
run: npm outdated --registry=https://registry.npmjs.org || true

View File

@@ -75,7 +75,10 @@ jobs:
run: |
cd ${{ vars.REMOTE_DIR || '/opt/gongxue' }}
sleep 3
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/api/dashboard/stats || true)"
# 从 .env 读 PORT默认 3000避免硬编码端口与后端不一致
PORT="$(grep '^PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
PORT="${PORT:-3000}"
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/api/dashboard/stats" || true)"
echo "后端 HTTP 状态: ${code}401=正常,接口需登录)"
if [ "${code}" != "401" ] && [ "${code}" != "200" ]; then
echo "健康检查失败:后端未按预期响应" >&2

View File

@@ -14,9 +14,9 @@
},
"dependencies": {
"@ant-design/icons": "^6.1.1",
"@ant-design/x": "^2.8.0",
"@ant-design/x": "^2.9.0",
"@ant-design/x-card": "^2.9.0",
"@ant-design/x-markdown": "^2.8.0",
"@ant-design/x-markdown": "^2.9.0",
"@ant-design/x-sdk": "^2.8.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -26,10 +26,11 @@
"antd": "^6.3.6",
"axios": "^1.15.1",
"dayjs": "^1.11.20",
"dompurify": "^3.4.13",
"echarts": "^6.0.0",
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.5",
"mermaid": "^11.16.0",
"mermaid": "^11.16.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router": "^8.3.0",

View File

@@ -1,9 +1,18 @@
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
import { join } from 'path';
import { basename, join, resolve } from 'path';
// TypeORM CLI runs from apps/server/
const root = process.cwd();
// TypeORM CLI 默认在 apps/server/ 下运行,但生产环境的 .env 通常放在仓库根目录。
// 这里先加载仓库根目录的 .env默认再加载当前目录apps/server/.env作为补充/覆盖,
// 这样无论 .env 放哪一份,迁移 CLI 都能读到数据库配置。
const root = resolve(process.cwd());
const repoRoot =
basename(root) === 'server' && basename(join(root, '..')) === 'apps'
? resolve(root, '../..')
: root;
// 先根目录后当前目录dotenv 默认不覆盖已存在变量,所以根目录优先级更高)
config({ path: join(repoRoot, '.env') });
config({ path: join(root, '.env') });
export default new DataSource({

View File

@@ -6,7 +6,9 @@ export function toMinutes(time: string): number {
}
export function getCourseClock(date: Date): { date: string; minutes: number } {
const d = dayjs(date);
// Asia/Shanghai 无夏令时,固定 UTC+8与 attendance-settlement 保持一致),
// 避免依赖服务器进程时区导致「课程是否已开始」判断漂移。
const d = dayjs(date).utcOffset(8);
return {
date: d.format('YYYY-MM-DD'),
minutes: d.hour() * 60 + d.minute(),

108
deploy.sh
View File

@@ -1,12 +1,84 @@
#!/usr/bin/env bash
# 恭学教育 — PM2 部署脚本
# 用法: ./deploy.sh [ssh_host]
# 用法:
# ./deploy.sh # 本地直跑(默认,适合 runner/代码就在生产服务器上,不需要 SSH
# ./deploy.sh local # 同上
# ./deploy.sh <ssh_host> # 远程部署(构建后 rsync 到远端,需要 SSH 免密/key
# 前提: 服务器 MySQL 已在运行PM2 已安装
set -euo pipefail
SSH_HOST="${1:-tencent}"
MODE="${1:-local}"
# 校验 SSH_HOST非空、不以 - 开头(防被 ssh/rsync 当作选项)、仅允许安全字符(防命令注入)
# 调用锁mkdir 原子创建,已存在即代表已有部署在跑,直接退出
LOCK_DIR="/tmp/gongxue-deploy.lock"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "检测到 $LOCK_DIR,已有部署在运行。若确认没有其他部署,请手动删除该锁目录。" >&2
exit 1
fi
release_deploy_lock() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap release_deploy_lock EXIT
echo "=== 1/4 本地构建后端 ==="
npm run build -w @gongxue/server
echo "=== 2/4 本地构建前端 ==="
npm run build -w @gongxue/admin
# ── 健康检查(本地/远程共用逻辑,通过 pm2 jlist 按 name 判断 online 且无 unstable_restarts──
# online ≠ healthy若后续后端提供健康端点应优先 curl 探测。
run_health_check() {
set -euo pipefail
local healthy=0
local i
for i in $(seq 1 15); do
if pm2 jlist 2>/dev/null | node -e '
let data = "";
process.stdin.on("data", (c) => (data += c));
process.stdin.on("end", () => {
const apps = JSON.parse(data || "[]");
const app = apps.find((a) => a && a.name === "gongxue-backend");
const env = (app || {}).pm2_env || {};
if (env.status === "online" && Number(env.unstable_restarts || 0) === 0) {
process.exit(0);
}
process.exit(1);
});
'; then
healthy=1
break
fi
sleep 2
done
if [ "${healthy}" -ne 1 ]; then
echo "错误后端进程未就绪gongxue-backend 非 online 或存在不稳定重启)" >&2
pm2 status >&2
return 1
fi
echo "健康检查通过gongxue-backend 在线"
}
if [ "${MODE}" = "local" ]; then
echo "=== 3/4 安装依赖 → 迁移 → PM2 重载(本机) ==="
npm ci
echo '执行数据库迁移...'
npm run migration:run -w @gongxue/server
echo 'PM2 重载...'
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
pm2 status
echo "=== 4/4 健康检查 ==="
run_health_check
echo ""
echo "部署完成!后端: http://127.0.0.1:$(grep '^PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || echo 3000)/api"
exit 0
fi
# ── 远程模式(保留,用于部署到其他主机)──
SSH_HOST="${MODE}"
# 校验 SSH_HOST非空、不以 - 开头、仅允许安全字符(防注入)
if [[ -z "${SSH_HOST}" ]]; then
echo "错误SSH_HOST 不能为空" >&2
exit 1
@@ -21,23 +93,6 @@ if [[ ! "${SSH_HOST}" =~ ^[A-Za-z0-9._:@%+=,-]+$ ]]; then
fi
REMOTE_DIR="/opt/gongxue"
# 调用锁mkdir 原子创建,已存在即代表已有部署在跑,直接退出
LOCK_DIR="/tmp/gongxue-deploy.lock"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "检测到 $LOCK_DIR,已有部署在运行。若确认没有其他部署,请手动删除该锁目录。" >&2
exit 1
fi
release_deploy_lock() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap release_deploy_lock EXIT
echo "=== 1/5 本地构建后端 ==="
npm run build -w @gongxue/server
echo "=== 2/5 本地构建前端 ==="
npm run build -w @gongxue/admin
echo "=== 3/5 同步到 ${SSH_HOST} ==="
# --delete 保留,但必须排除服务器端独有文件(.env、数据、上传目录防止被清掉
rsync -avz --delete \
@@ -66,18 +121,11 @@ ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" "
echo 'PM2 重载...'
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
echo '=== PM2 状态 ==='
pm2 status
"
echo "=== 5/5 健康检查 ==="
# 后端未提供 /api/health 等专用健康端点,改用 pm2 jlistJSON按 name 查找 gongxue-backend
# 判断 status === 'online' 且 unstable_restarts === 0。
# 说明:
# - restart_time 是累计重启次数(正常滚动/长期运行也会累计),不适合做健康阈值,改用 unstable_restarts。
# - online ≠ healthyonline 只代表 PM2 认为进程存活;若后续后端提供健康端点,应优先 curl 探测。
# 带重试以覆盖 PM2 reload 的 listen_timeout8s窗口应用不在线则 exit 1部署失败。
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" bash -s <<'REMOTE_HEALTH'
ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" bash -s <<'REMOTE_HEALTH'
set -euo pipefail
HEALTHY=0
for _i in $(seq 1 15); do
@@ -106,10 +154,6 @@ if [ "${HEALTHY}" -ne 1 ]; then
fi
echo "健康检查通过gongxue-backend 在线"
REMOTE_HEALTH
then
echo "错误:健康检查失败,部署中止" >&2
exit 1
fi
echo ""
echo "部署完成!"

214
package-lock.json generated
View File

@@ -21,9 +21,9 @@
"version": "0.0.0",
"dependencies": {
"@ant-design/icons": "^6.1.1",
"@ant-design/x": "^2.8.0",
"@ant-design/x": "^2.9.0",
"@ant-design/x-card": "^2.9.0",
"@ant-design/x-markdown": "^2.8.0",
"@ant-design/x-markdown": "^2.9.0",
"@ant-design/x-sdk": "^2.8.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -33,10 +33,11 @@
"antd": "^6.3.6",
"axios": "^1.15.1",
"dayjs": "^1.11.20",
"dompurify": "^3.4.13",
"echarts": "^6.0.0",
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.5",
"mermaid": "^11.16.0",
"mermaid": "^11.16.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router": "^8.3.0",
@@ -65,6 +66,103 @@
"vitest": "^4.1.10"
}
},
"apps/admin/node_modules/@ant-design/x": {
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/@ant-design/x/-/x-2.9.0.tgz",
"integrity": "sha512-m3N2nprrPG/D0n93B/fBFhjpvSaJAb2IDF6w0rX0ycYXSGxtgrJQvqSI33Jhx6gxUHj495zdIzqA1NMFld9iQA==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/cssinjs": "^2.0.1",
"@ant-design/cssinjs-utils": "^2.0.2",
"@ant-design/fast-color": "^3.0.0",
"@ant-design/icons": "^6.0.0",
"@babel/runtime": "^7.25.6",
"@rc-component/motion": "^1.1.6",
"@rc-component/resize-observer": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1",
"lodash.throttle": "^4.1.1",
"mermaid": "^11.12.1",
"react-syntax-highlighter": "^16.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design"
},
"peerDependencies": {
"antd": "^6.1.1",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"apps/admin/node_modules/@ant-design/x-markdown": {
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/@ant-design/x-markdown/-/x-markdown-2.9.0.tgz",
"integrity": "sha512-IwYUky9Fi3y86wouFQ3rjx/DyKFUm/eAidiU/leVD0u6hAfN4INDWltWuZqe1FRRwOE7DJdSxweDyD6+/B6oQg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1",
"dompurify": "^3.2.6",
"html-react-parser": "^5.2.13",
"katex": "^0.16.22",
"marked": "^15.0.12"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"apps/admin/node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"apps/admin/node_modules/mermaid": {
"version": "11.16.1",
"resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.16.1.tgz",
"integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.2",
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "^1.2.0",
"@types/d3": "^7.4.3",
"@upsetjs/venn.js": "^2.0.0",
"cytoscape": "^3.33.3",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.14",
"dayjs": "^1.11.20",
"dompurify": "^3.3.3",
"es-toolkit": "^1.45.1",
"katex": "^0.16.45",
"khroma": "^2.1.0",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0"
}
},
"apps/admin/node_modules/mermaid/node_modules/marked": {
"version": "16.4.2",
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"apps/admin/node_modules/react-router": {
"version": "8.3.0",
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-8.3.0.tgz",
@@ -553,36 +651,6 @@
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@ant-design/x": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@ant-design/x/-/x-2.8.0.tgz",
"integrity": "sha512-z0ncy8i3sy8fnFS1iETTQfP2Os1kzOl5OPdljT5ZFSCDsZp+VYFt9jHHMacBnkEftKNzkmG1BLSwvBTS1aWxiw==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/cssinjs": "^2.0.1",
"@ant-design/cssinjs-utils": "^2.0.2",
"@ant-design/fast-color": "^3.0.0",
"@ant-design/icons": "^6.0.0",
"@babel/runtime": "^7.25.6",
"@rc-component/motion": "^1.1.6",
"@rc-component/resize-observer": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1",
"lodash.throttle": "^4.1.1",
"mermaid": "^11.12.1",
"react-syntax-highlighter": "^16.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design"
},
"peerDependencies": {
"antd": "^6.1.1",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@ant-design/x-card": {
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/@ant-design/x-card/-/x-card-2.9.0.tgz",
@@ -599,23 +667,6 @@
"react-dom": ">=18.0.0"
}
},
"node_modules/@ant-design/x-markdown": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@ant-design/x-markdown/-/x-markdown-2.8.0.tgz",
"integrity": "sha512-QRS0s81ykVt8RLqf9nGHJmP/y8hNQgQZdu7+7x+EMS5VuPBeQw74IpIUNchVLQy/P60VoNpkw+YTalyJtyLPPw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1",
"dompurify": "^3.2.6",
"html-react-parser": "^5.2.13",
"katex": "^0.16.22",
"marked": "^15.0.12"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@ant-design/x-sdk": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@ant-design/x-sdk/-/x-sdk-2.8.0.tgz",
@@ -10085,15 +10136,6 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -14394,60 +14436,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/mermaid": {
"version": "11.16.0",
"resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.16.0.tgz",
"integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.2",
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "^1.2.0",
"@types/d3": "^7.4.3",
"@upsetjs/venn.js": "^2.0.0",
"cytoscape": "^3.33.3",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.14",
"dayjs": "^1.11.20",
"dompurify": "^3.3.3",
"es-toolkit": "^1.45.1",
"katex": "^0.16.45",
"khroma": "^2.1.0",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0"
}
},
"node_modules/mermaid/node_modules/marked": {
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/mermaid/node_modules/uuid": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",

View File

@@ -36,6 +36,7 @@
},
"minimatch@10.2.5": {
"brace-expansion": "^5.0.9"
}
},
"dompurify": "^3.4.13"
}
}