From 1eb5a13df791a9985d89eb25aa8fe859dc644dd3 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 1 Jul 2026 04:54:07 +0800 Subject: [PATCH] test: strengthen launch security and capacity gates --- README.md | 21 +- ...ackend-open-items-and-capacity-20260701.md | 18 +- docs/refactor/frontend-handoff-index.md | 4 +- docs/refactor/next-development-todo.md | 4 +- .../production-launch-evidence.template.json | 33 ++- .../taro-production-integration-checklist.md | 3 +- .../web-launch-acceptance-checklist.md | 5 +- package.json | 3 +- scripts/production-launch-gate-test.js | 34 +++ scripts/production-launch-gate.js | 26 +- scripts/repo-security-scan-test.js | 59 ++++ scripts/repo-security-scan.js | 261 ++++++++++++++++++ scripts/taro-h5-interaction-smoke.js | 12 + 13 files changed, 452 insertions(+), 31 deletions(-) create mode 100644 scripts/repo-security-scan-test.js create mode 100644 scripts/repo-security-scan.js diff --git a/README.md b/README.md index 217dea5c..4652dcf7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ | 对象存储/资料安全 | √ 可联调,待生产 AV/CDN | OSS/COS/Supabase Storage 签名、上传确认、短签名预览下载、水印 traceId、复检和安全扫描地基已完成 | | PocketBase 真实数据迁移 | √ 本地跑通,待人工复核 blocker | SQLite 导出、标准化导入、校验和抽样脚本已跑通;正式切换前处理缺用户订单和缺归属手册章节 | | Taro H5 三端前端 | √ 第一版可构建 | 学生端、租户后台、平台后台均有真实 API 页面;已补 H5 `index.html` 模板和发布产物守卫;后续继续补小程序兼容、视觉精修、状态管理、包体优化和端到端测试 | -| 生产安全/压测交付 | △ 本地真实数据压测已跑,云端待复测 | 本地 Docker/Supabase 已完成真实迁移数据 30/50/100/150 并发只读和混合读写压测;受限 API 容器复核中抓到并修复了自动勋章并发发放唯一键冲突,并补并发回归测试。最近一次只读上线门禁和 50/100/150 混合读写均 0 错误,50/100 混合读写约 418-420 req/s,150 并发 P95 超 600ms 进入压力区;上云后仍需执行生产 readiness、远程 Auth/RLS、4c16g 压测、PostgreSQL 调优和上线证据门禁。`@codex-security` 只有在 Codex 环境暴露扫描工具时才能标记完成 | +| 生产安全/压测交付 | △ 本地真实数据压测已跑,云端待复测 | 本地 Docker/Supabase 已完成真实迁移数据 30/50/100/150 并发只读和混合读写压测;受限 API 容器复核中抓到并修复了自动勋章并发发放唯一键冲突,并补并发回归测试。最近一次只读上线门禁和 50/100/150 混合读写均 0 错误,50/100 混合读写约 428-444 req/s,150 并发 P95 约 574ms 进入压力区;上云后仍需执行生产 readiness、远程 Auth/RLS、4c16g 压测、PostgreSQL 调优、`security:repo`、真实 `@codex-security` 扫描和上线证据门禁。 | 更完整的进度看这些文档: @@ -153,7 +153,7 @@ npm run smoke:taro:h5:interaction `taro-route-contract-test` 会校验 `apps/taro/src/app.config.ts`、真实 `pages/**/index.tsx`、启动页三端跳转、H5 静态烟测入口和前端交接文档中的页面引用保持一致。新增或删除页面时必须同步路由和文档,避免 H5/小程序构建后才发现入口漂移。 `taro-api-contract-test` 会比对 `apps/taro/src` 中所有 `apiRequest('/api/...')` 调用与 `apps/api/src/features/*/index.ts` 注册路由,阻断前端调用不存在 API、method 写错或绕过统一 `/api` 命名空间的漂移;动态导入和少量 server alias 需要在脚本 allowlist 中显式声明。 `taro-persona-contract-test` 会从学生、租户管理员、平台管理员三类前端视角检查关键页面、路由和服务调用,阻断刷题、会员订单、错题收藏、学生运营、内容导入、营销财务、租户设置、平台租户账务和公共题库授权入口被误删或漂移。 -`smoke:taro:h5:interaction` 会启动三套 H5 发布产物、本地 mock API 和本机 Chrome/Edge,在真实浏览器里点击学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台,租户后台六个主模块,以及平台后台四个主模块,用来补足静态烟测无法发现的 H5 运行时空白页、history 路由和点击事件问题。 +`smoke:taro:h5:interaction` 会启动三套 H5 发布产物、本地 mock API 和本机 Chrome/Edge,在真实浏览器里点击学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块,用来补足静态烟测无法发现的 H5 运行时空白页、history 路由和点击事件问题。 H5 线上推荐每个静态目录放独立 `runtime-config.json` 覆盖公开配置,避免 API/Auth 域名变化时重打包: @@ -393,6 +393,7 @@ npm run test:auth:remote-smoke npm run test:launch-gate npm run smoke:auth:remote npm run audit:runtime +npm run security:repo npm run pb:import:dry-run npm run pb:import:validate npm run test:pb:dry-run @@ -468,10 +469,10 @@ npm run launch:gate -- --evidence docs/refactor/production-launch-evidence.json - PocketBase production dry-run、`pb:import:validate`、`pb:import:sample`。 - 真实数据 API 读路径压测、API/worker/Taro 构建。 - `smoke:taro:h5`、`smoke:taro:h5:interaction`、严格 `taro-h5-release-guardrails-test --require-runtime-config`。 -- `audit:runtime`、真实 `@codex-security` 扫描。 +- `audit:runtime`、`security:repo`、真实 `@codex-security` 扫描。 - 备份、回滚、真实数据抽样、生产 provider、对象存储控制、支付对账和三套 H5 `runtime-config.json` 人工确认。 -补充说明:当前 Codex 环境如果没有暴露 `@codex-security` 可调用工具,不能把插件扫描写成已完成;只能先用 `npm run audit:runtime`、`npm run test:readiness`、`npm run test:rls` 和代码审查作为临时安全证据,并在上线证据里保留插件扫描待补项。 +补充说明:当前 Codex 环境如果没有暴露 `@codex-security` 可调用工具,不能把插件扫描写成已完成;只能先用 `npm run audit:runtime`、`npm run security:repo`、`npm run test:readiness`、`npm run test:rls` 和代码审查作为临时安全证据,并在上线证据里保留插件扫描待补项。 模板文件: @@ -616,7 +617,7 @@ npm run smoke:taro:h5:interaction node scripts\taro-h5-release-guardrails-test.js --require-dist ``` -`smoke:taro:h5` 会启动临时静态服务器和 mock API,验证三套 H5 的 `index.html`、静态资源、history fallback、公开 runtime config 和租户解析契约。`smoke:taro:h5:interaction` 会再拉起真实 Chrome/Edge,打开三套 H5 产物并点击 25 项关键入口,覆盖学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台,租户后台六个主模块,以及平台后台四个主模块,确认页面 JS 执行、路由跳转、关键 API 和后台入口点击没有空白页或运行时异常。`taro-h5-release-guardrails-test` 会确认三套 H5 目录存在 `index.html`,并扫描源码/产物是否混入旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串或服务端密钥形态。正式部署时还必须在每个 H5 目录根部放置对应的 `runtime-config.json`。 +`smoke:taro:h5` 会启动临时静态服务器和 mock API,验证三套 H5 的 `index.html`、静态资源、history fallback、公开 runtime config 和租户解析契约。`smoke:taro:h5:interaction` 会再拉起真实 Chrome/Edge,打开三套 H5 产物并点击 26 项关键入口,覆盖学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块,确认页面 JS 执行、路由跳转、关键 API 和后台入口点击没有空白页或运行时异常。`taro-h5-release-guardrails-test` 会确认三套 H5 目录存在 `index.html`,并扫描源码/产物是否混入旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串或服务端密钥形态。正式部署时还必须在每个 H5 目录根部放置对应的 `runtime-config.json`。 写入生产上线证据时使用严格模式,确保三套正式发布目录已经放好真实公开 `runtime-config.json`,且 warning 为 0: @@ -653,10 +654,10 @@ node scripts\taro-h5-release-guardrails-test.js --require-dist --require-runtime | 50 | 60s | 10% | 35,608 | 0.00% | 584.18 req/s | 174.26 ms | 224.04 ms | | 100 | 60s | 8% | 33,843 | 0.00% | 554.42 req/s | 326.21 ms | 422.33 ms | | 150 | 60s | 6% | 32,364 | 0.00% | 528.81 req/s | 490.38 ms | 662.42 ms | -| Docker API 2c4g / 30 | 120s | 0% | 44,038 | 0.00% | 366.27 req/s | 185.80 ms | 254.08 ms | -| Docker API 2c4g / 50 | 60s | 10% | 25,327 | 0.00% | 419.62 req/s | 213.17 ms | 270.94 ms | -| Docker API 2c4g / 100 | 60s | 8% | 25,378 | 0.00% | 417.77 req/s | 383.72 ms | 461.22 ms | -| Docker API 2c4g / 150 | 60s | 6% | 23,444 | 0.00% | 384.39 req/s | 605.89 ms | 743.19 ms | +| Docker API 2c4g / 30 | 120s | 0% | 44,692 | 0.00% | 371.62 req/s | 188.91 ms | 275.79 ms | +| Docker API 2c4g / 50 | 60s | 10% | 26,815 | 0.00% | 444.49 req/s | 204.08 ms | 268.88 ms | +| Docker API 2c4g / 100 | 60s | 8% | 25,950 | 0.00% | 428.30 req/s | 375.38 ms | 447.47 ms | +| Docker API 2c4g / 150 | 60s | 6% | 23,970 | 0.00% | 394.80 req/s | 574.37 ms | 696.35 ms | 只读上线门禁继续要求 `includeWrites=false`;混合读写报告需要显式使用 `--allow-writes` 做人工容量观察,例如: @@ -666,7 +667,7 @@ npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark- 使用 `--allow-writes` 时会输出 `capacityObservation`,不输出 `launchGateCheck`,不能把写入场景误填成生产上线门禁的只读证据。 -本地结论:当前 Docker Desktop 分配 20 CPU、约 62.7GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 03:58 受限 API 容器复核中,30 worker/120 秒只读上线门禁为 44,038 请求、0 错误、366.27 req/s、P95 185.80ms;50 worker/60 秒/10% 写入为 25,327 请求、0 错误、419.62 req/s、P95 213.17ms;100 worker/60 秒/8% 写入为 25,378 请求、0 错误、417.77 req/s、P95 383.72ms;150 worker/60 秒/6% 写入为 23,444 请求、0 错误、384.39 req/s、P95 605.89ms,属于压力区。该轮压测曾抓到自动勋章并发发放唯一键冲突,已修复并新增并发回归测试。按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,当前受限 API 容器舒适观察区间约对应 2,000 到 8,400 名活跃在线学生的请求吞吐。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见: +本地结论:当前 Docker Desktop 分配 20 CPU、约 62.7GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 04:44 受限 API 容器复核中,30 worker/120 秒只读上线门禁为 44,692 请求、0 错误、371.62 req/s、P95 188.91ms;50 worker/60 秒/10% 写入为 26,815 请求、0 错误、444.49 req/s、P95 204.08ms;100 worker/60 秒/8% 写入为 25,950 请求、0 错误、428.30 req/s、P95 375.38ms;150 worker/60 秒/6% 写入为 23,970 请求、0 错误、394.80 req/s、P95 574.37ms,属于压力区。该受限容器系列压测曾抓到自动勋章并发发放唯一键冲突,已修复并新增并发回归测试。按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,当前受限 API 容器舒适观察区间约对应 2,100 到 8,900 名活跃在线学生的请求吞吐。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见: ```text docs/refactor/performance-benchmark-summary-20260630.md diff --git a/docs/refactor/backend-open-items-and-capacity-20260701.md b/docs/refactor/backend-open-items-and-capacity-20260701.md index 31705512..c1595930 100644 --- a/docs/refactor/backend-open-items-and-capacity-20260701.md +++ b/docs/refactor/backend-open-items-and-capacity-20260701.md @@ -43,7 +43,7 @@ - 环境:Windows + Docker Desktop + 本地 Supabase/PostgreSQL + 本地 API 进程。 - Docker Desktop 当前资源:20 CPU、约 62.7GB 内存。这个本机结果会高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。 - 数据库:PocketBase 真实导入数据,并已包含前期写入压测产生的练习/答题记录。 -- 压测基线规模:约 7.4 万道题、3,700 个用户、1,600 个题目合集、3,100 个练习蓝图、3,500 个单词、2,600 条知识手册、19.6 万条答题记录、3.8 万条错题、466 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按压测基线描述,不把每次测试后的瞬时行数当作 SLA 口径。 +- 当前本地迁移库规模:约 7.4 万道题、3,700 个用户、1,600 个题目合集、3,100 个练习蓝图、3,500 个单词、2,600 条知识手册、25.2 万条答题记录、3.8 万条错题、468 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按当前本地观察描述,不把每次测试后的瞬时行数当作生产 SLA 口径。 - 排行榜未纳入默认负载,因为产品默认关闭。 ### 2026-07-01 真实迁移库 Docker API 受限资源复核 @@ -54,10 +54,10 @@ | 场景 | 并发 worker | 时长 | 刷题写入比例 | 请求数 | 错误率 | 吞吐 | P95 | P99 | 结论 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | -| 只读上线门禁 | 30 | 120s | 0% | 44,038 | 0.00% | 366.27 req/s | 185.80 ms | 254.08 ms | 通过 | -| 混合读写 | 50 | 60s | 10% | 25,327 | 0.00% | 419.62 req/s | 213.17 ms | 270.94 ms | 舒适 | -| 混合读写 | 100 | 60s | 8% | 25,378 | 0.00% | 417.77 req/s | 383.72 ms | 461.22 ms | 接近舒适区上沿 | -| 混合读写 | 150 | 60s | 6% | 23,444 | 0.00% | 384.39 req/s | 605.89 ms | 743.19 ms | 压力区 | +| 只读上线门禁 | 30 | 120s | 0% | 44,692 | 0.00% | 371.62 req/s | 188.91 ms | 275.79 ms | 通过 | +| 混合读写 | 50 | 60s | 10% | 26,815 | 0.00% | 444.49 req/s | 204.08 ms | 268.88 ms | 舒适 | +| 混合读写 | 100 | 60s | 8% | 25,950 | 0.00% | 428.30 req/s | 375.38 ms | 447.47 ms | 接近舒适区上沿 | +| 混合读写 | 150 | 60s | 6% | 23,970 | 0.00% | 394.80 req/s | 574.37 ms | 696.35 ms | 压力区 | 混合读写包含真实刷题闭环:创建练习 session、拉取 session detail、提交答案、交卷、读取报告。 @@ -69,10 +69,10 @@ 按目前常见页面节奏先用 `0.05 到 0.2 req/s/人` 粗略折算: -- 50 worker 舒适场景 420 req/s,约等于 2,100 到 8,400 名活跃在线学生的请求吞吐。 -- 100 worker 上沿场景 418 req/s,约等于 2,090 到 8,360 名活跃在线学生的请求吞吐。 -- 150 worker 压力场景 384 req/s,约等于 1,920 到 7,680 名活跃在线学生的请求吞吐。 +- 50 worker 舒适场景 444 req/s,约等于 2,220 到 8,880 名活跃在线学生的请求吞吐。 +- 100 worker 上沿场景 428 req/s,约等于 2,140 到 8,560 名活跃在线学生的请求吞吐。 +- 150 worker 压力场景 395 req/s,约等于 1,975 到 7,900 名活跃在线学生的请求吞吐。 -当前本地结论:在受限 API 容器下,后端真实刷题读写链路的舒适观察区间约为 418 到 420 req/s,折算约 2,000 到 8,400 名活跃在线学生;150 worker 仍 0 错误但 P95 已超过 600ms,视为压力区,不建议作为生产承诺。此前未限制 API 容器时本机可观察到约 550 到 585 req/s,说明 API CPU/连接池约束会明显影响上沿。 +当前本地结论:在受限 API 容器下,后端真实刷题读写链路的舒适观察区间约为 428 到 444 req/s,折算约 2,100 到 8,900 名活跃在线学生;150 worker 仍 0 错误但 P95 已接近 600ms,视为压力区,不建议作为生产承诺。同日本地 API 进程 30 worker/30 秒/10% 写入观察到 15,864 请求、0 错误、513.56 req/s、P95 113.97ms;受限 API 容器会明显压低上沿,因此上线承诺仍以云端受限资源复测为准。 正式对外容量必须等 4 核 16G 云服务器部署后复跑。保守规划时,生产首版建议先按本地折算值的 30% 到 50% 做容量承诺,等云端压测、CDN、对象存储和真实前端埋点完成后再上调。 diff --git a/docs/refactor/frontend-handoff-index.md b/docs/refactor/frontend-handoff-index.md index 059404b4..625de62d 100644 --- a/docs/refactor/frontend-handoff-index.md +++ b/docs/refactor/frontend-handoff-index.md @@ -46,10 +46,10 @@ - 新增、删除或重命名 Taro 页面时必须同步 `apps/taro/src/app.config.ts`、启动页跳转、H5 静态烟测入口和本文页面清单,并运行 `node scripts/taro-route-contract-test.js`。该脚本会阻断“页面文件存在但未注册”“路由注册但文件缺失”“启动页或烟测跳到不存在页面”的漂移。 - 新增或修改 Taro API service 时必须运行 `node scripts/taro-api-contract-test.js`。该脚本会比对前端 `apiRequest('/api/...')` 与后端 `RouteDefinition[]` 注册表,阻断调用不存在的接口、method 写错或绕过统一 API client;动态路由只能通过脚本 allowlist 明确声明。 - 修改学生端、租户后台或平台后台关键页面时必须运行 `node scripts/taro-persona-contract-test.js`。该脚本按学生刷题/会员订单/错题收藏、租户学生运营/内容导入/营销财务/品牌权限、平台租户/账务/公共题库/员工权限三类角色旅程检查路由和服务调用,避免前端样式重做时误删核心业务入口。 -- H5 构建完成后必须运行 `npm run smoke:taro:h5:interaction` 做真实浏览器点击验证。它会覆盖学生首页到题库练习、答题、收藏、个人中心开通会员,租户后台工作台到题库内容/财务运营,以及平台后台工作台到租户管理/账务中心;如果 Chrome/Edge 缺失,可设置 `TARO_H5_SMOKE_BROWSER` 指向 Chromium 浏览器。 +- H5 构建完成后必须运行 `npm run smoke:taro:h5:interaction` 做真实浏览器点击验证。它会覆盖学生首页到题库练习、答题、收藏、会员收银台下单/支付参数/订单状态,租户后台工作台到题库内容/财务运营,以及平台后台工作台到租户管理/账务中心;如果 Chrome/Edge 缺失,可设置 `TARO_H5_SMOKE_BROWSER` 指向 Chromium 浏览器。 - H5 可以优先验证 `@supabase/supabase-js` 管理 Auth session;微信小程序端先验证运行时兼容性,业务数据默认仍走 `apps/api`。 - H5 生产部署优先用每个静态目录自己的 `runtime-config.json` 配置 `apiBaseUrl`、`supabaseUrl`、`supabasePublishableKey`、`tenantCode`;不要为了换域名重打包,也不要把任何 service role、数据库、支付、短信、对象存储密钥放进该文件。 -- 上线前需要把三套 H5 构建、`npm run smoke:taro:h5` 静态启动烟测、`npm run smoke:taro:h5:interaction` 真实浏览器交互烟测、严格 `taro-h5-release-guardrails-test --require-runtime-config`、`runtime-config.json` 人工复核、真实 Auth/RLS、迁移 dry-run、对象存储、支付对账和真实 `@codex-security` 结果写入 `production-launch-evidence.json`,并通过 `npm run launch:gate`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。 +- 上线前需要把三套 H5 构建、`npm run smoke:taro:h5` 静态启动烟测、`npm run smoke:taro:h5:interaction` 真实浏览器交互烟测、严格 `taro-h5-release-guardrails-test --require-runtime-config`、`runtime-config.json` 人工复核、真实 Auth/RLS、迁移 dry-run、对象存储、支付对账、`security:repo` 和真实 `@codex-security` 结果写入 `production-launch-evidence.json`,并通过 `npm run launch:gate`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。 - 可以接入租户品牌、已发布主题、公开素材、功能开关和域名/小程序参数解析;学生端只读 `/api/tenant/resolve` 的 `branding.theme/publicAssets`,租户后台草稿走 `/api/tenant-admin/theme`。 - 租户后台可以接入角色模板和成员 API:`/api/tenant-admin/role-templates`、`/api/tenant-admin/members`,用于运营、教师、销售、代理等自定义菜单/模块/字段可见性和成员模板绑定。 - 租户后台可以接入勋章管理、手动发放、积分任务、每日签到奖励、积分兑换、积分风控只读报表和反馈运营报表:`GET/PUT /api/tenant-admin/badges`、`GET/POST /api/tenant-admin/badge-grants`、`GET/PUT /api/tenant-admin/point-activity-tasks`、`GET /api/tenant-admin/point-activity-claims`、`GET/PUT /api/tenant-admin/point-exchange-items`、`GET /api/tenant-admin/point-exchange-orders`、`GET /api/tenant-admin/points-risk-report`、`GET /api/tenant-admin/feedbacks/report`;学生端用 `GET /api/profile/badges` 展示成就,通过 `POST /api/profile/check-in` 自动领取 `daily_check_in` 任务奖励,并通过 `GET /api/profile/activity-tasks`、`POST /api/profile/activity-tasks/claim`、`GET /api/profile/exchange-items`、`POST /api/profile/exchange-items/redeem` 接其它积分活动和兑换。 diff --git a/docs/refactor/next-development-todo.md b/docs/refactor/next-development-todo.md index 75a1e36f..04008e0d 100644 --- a/docs/refactor/next-development-todo.md +++ b/docs/refactor/next-development-todo.md @@ -91,10 +91,10 @@ - 生产 `.env` 模板和 `npm run readiness:production` / `npm run readiness:production:db` 已补,后续上云必须作为验收 gate。 - Auth/JWKS 上云后必须临时设置 `AUTH_SMOKE_*` 环境变量并运行 `npm run smoke:auth:remote`,真实 access token 不得写入仓库、前端配置或日志。 - 本地/预生产必须同时跑 `npm run test:rls`,它验证运行时 JWT claim 下的租户隔离,和 `readiness:production:db` 的静态 policy 检查互补。 - - 已补 `npm run launch:gate` 生产上线证据门禁和 `docs/refactor/production-launch-evidence.template.json` 模板;最终切换前必须把 readiness、远程 Auth、RLS、PostgreSQL 4c16g 严格调参证据、生产 dry-run、导入校验、`pb:import:sample` 业务抽样、真实数据 API 读路径压测、API/worker/Taro、运行时审计、`@codex-security`、备份/回滚/真实抽样/生产 provider 等证据填入本地 `production-launch-evidence.json` 并通过门禁。当前 Codex 环境未暴露可调用的 `@codex-security` 扫描工具时,该项只能标为待补,不能伪造完成。 + - 已补 `npm run launch:gate` 生产上线证据门禁和 `docs/refactor/production-launch-evidence.template.json` 模板;最终切换前必须把 readiness、远程 Auth、RLS、PostgreSQL 4c16g 严格调参证据、生产 dry-run、导入校验、`pb:import:sample` 业务抽样、真实数据 API 只读压测、真实数据混合读写压测、API/worker/Taro、运行时审计、`security:repo`、真实 `@codex-security`、备份/回滚/真实抽样/生产 provider 等证据填入本地 `production-launch-evidence.json` 并通过门禁。当前 Codex 环境未暴露可调用的 `@codex-security` 扫描工具时,该项只能标为待补,不能伪造完成。 - 确认数据库迁移流程、备份恢复、日志、告警。 - 准备 API 容器部署和 Supabase 云端/自托管连接方案。 - - 已补 `npm run perf:api:local`、`npm run perf:summary`、`npm run perf:postgres:evidence`、`npm run perf:postgres:sql`、`npm run smoke:launch-persona`、`npm run smoke:taro:h5`、`npm run smoke:taro:h5:interaction`、`node scripts/taro-api-contract-test.js`、`node scripts/taro-persona-contract-test.js` 和 `docs/refactor/performance-benchmark-runbook.md`,可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 4c16g profile 调参证据、三类后端角色旅程烟测、三类 Taro 前端角色旅程契约、H5 发布目录启动烟测、真实浏览器关键点击烟测和前端 API 契约检查。2026-07-01 03:58 受限 API 容器真实迁移库复核中,30 worker/120s 只读为 44,038 请求、0 错误、366.27 req/s、P95 185.80ms、P99 254.08ms;50 worker/60s/10% 写入为 25,327 请求、0 错误、419.62 req/s、P95 213.17ms、P99 270.94ms;100 worker/60s/8% 写入为 25,378 请求、0 错误、417.77 req/s、P95 383.72ms、P99 461.22ms;150 worker/60s/6% 写入为 23,444 请求、0 错误、384.39 req/s、P95 605.89ms、P99 743.19ms。压测先抓到自动勋章并发发放唯一键冲突,已修复并新增 `scripts/auto-badge-concurrency-test.js`。当前 Docker Desktop 给了 20 CPU/约 62.7GB 内存,但 API 容器限制为 2 CPU/4G,舒适观察区暂按 50 到 100 个无停顿 worker 估算,150 worker 已是压力区;按单学生 0.05 到 0.2 req/s 粗略折算约为 2,000 到 8,400 名活跃在线学生的本机吞吐观察区间,正式容量仍以上云 4 核 16G 复测为准。 + - 已补 `npm run perf:api:local`、`npm run perf:summary`、`npm run perf:postgres:evidence`、`npm run perf:postgres:sql`、`npm run smoke:launch-persona`、`npm run smoke:taro:h5`、`npm run smoke:taro:h5:interaction`、`node scripts/taro-api-contract-test.js`、`node scripts/taro-persona-contract-test.js` 和 `docs/refactor/performance-benchmark-runbook.md`,可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 4c16g profile 调参证据、三类后端角色旅程烟测、三类 Taro 前端角色旅程契约、H5 发布目录启动烟测、真实浏览器关键点击烟测和前端 API 契约检查。2026-07-01 04:44 受限 API 容器真实迁移库复核中,30 worker/120s 只读为 44,692 请求、0 错误、371.62 req/s、P95 188.91ms、P99 275.79ms;50 worker/60s/10% 写入为 26,815 请求、0 错误、444.49 req/s、P95 204.08ms、P99 268.88ms;100 worker/60s/8% 写入为 25,950 请求、0 错误、428.30 req/s、P95 375.38ms、P99 447.47ms;150 worker/60s/6% 写入为 23,970 请求、0 错误、394.80 req/s、P95 574.37ms、P99 696.35ms。受限容器系列压测曾抓到自动勋章并发发放唯一键冲突,已修复并新增 `scripts/auto-badge-concurrency-test.js`。当前 Docker Desktop 给了 20 CPU/约 62.7GB 内存,但 API 容器限制为 2 CPU/4G,舒适观察区暂按 50 到 100 个无停顿 worker 估算,150 worker 已是压力区;按单学生 0.05 到 0.2 req/s 粗略折算约为 2,100 到 8,900 名活跃在线学生的本机吞吐观察区间,正式容量仍以上云 4 核 16G 复测为准。 - 当前本地 PostgreSQL evidence 仍提示 `jit=on`、`statement_timeout=0`、`idle_in_transaction_session_timeout=0`、`lock_timeout=0`;上云后必须按 `docs/refactor/postgresql-4c16g-tuning.md` 调整参数,执行 `PG_TUNING_PROFILE=shared-host npm run perf:postgres:evidence -- --strict --json` 并通过后,再按 6/30/50/100 阶梯并发复跑容量报告并归档到本地上线证据。 - 本轮剩余功能和容量复核已经整理到 `docs/refactor/backend-open-items-and-capacity-20260701.md`。后续不要再把学生头像上传或默认排行榜当作待办;头像只保留男女预设,排行榜仅作为租户显式开启后的活动能力。 diff --git a/docs/refactor/production-launch-evidence.template.json b/docs/refactor/production-launch-evidence.template.json index 0ac21fb9..0418637d 100644 --- a/docs/refactor/production-launch-evidence.template.json +++ b/docs/refactor/production-launch-evidence.template.json @@ -117,6 +117,22 @@ "includeWrites": false } }, + { + "id": "performance.api-real-data-mixed", + "status": "pass", + "command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=120 PERF_CONCURRENCY=50 PERF_RAMP_SECONDS=15 PERF_INCLUDE_WRITES=true PERF_PRACTICE_FLOW_RATIO=0.1 PERF_AUTH_MODE=app_session npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-mixed-benchmark.log", + "completedAt": "2026-06-30T10:49:00+08:00", + "artifact": "launch-artifacts/api-real-data-mixed-benchmark.log", + "summary": { + "errors": 0, + "errorRate": 0, + "p95Ms": 0, + "p99Ms": 0, + "concurrency": 50, + "durationSeconds": 120, + "includeWrites": true + } + }, { "id": "api.integration", "status": "pass", @@ -247,12 +263,14 @@ "artifact": "launch-artifacts/taro-h5-interaction-smoke.json", "summary": { "fail": 0, - "pass": 11, + "pass": 26, "mockApi": { "keyRequests": { "answers": 1, "favorites": 2, - "tenantResolve": 3 + "ordersCreated": 1, + "paymentsCreated": 1, + "orderStatus": 1 } } } @@ -279,6 +297,17 @@ "high": 0 } }, + { + "id": "security.repo-scan", + "status": "pass", + "command": "npm run security:repo -- --json > docs/refactor/launch-artifacts/repo-security-scan.json", + "completedAt": "2026-06-30T12:22:00+08:00", + "artifact": "launch-artifacts/repo-security-scan.json", + "summary": { + "critical": 0, + "high": 0 + } + }, { "id": "security.codex-scan", "status": "replace-with-pass-after-real-scan", diff --git a/docs/refactor/taro-production-integration-checklist.md b/docs/refactor/taro-production-integration-checklist.md index 074fbc02..be6b6a2e 100644 --- a/docs/refactor/taro-production-integration-checklist.md +++ b/docs/refactor/taro-production-integration-checklist.md @@ -73,9 +73,10 @@ npm run readiness:production:db npm run smoke:auth:remote npm run test:rls npm run audit:runtime +npm run security:repo ``` -正式上线前,三套 H5 严格发布证据、真实 Auth/RLS、真实 provider 抽样、对象存储控制、支付对账、PostgreSQL 严格调参证据和真实数据压测都要写入本地 `docs/refactor/production-launch-evidence.json`,再运行: +正式上线前,三套 H5 严格发布证据、真实 Auth/RLS、真实 provider 抽样、对象存储控制、支付对账、PostgreSQL 严格调参证据、`security:repo` 和真实数据压测都要写入本地 `docs/refactor/production-launch-evidence.json`,再运行: ```bash npm run launch:gate -- --evidence docs/refactor/production-launch-evidence.json diff --git a/docs/refactor/web-launch-acceptance-checklist.md b/docs/refactor/web-launch-acceptance-checklist.md index 8931458e..dfffc77b 100644 --- a/docs/refactor/web-launch-acceptance-checklist.md +++ b/docs/refactor/web-launch-acceptance-checklist.md @@ -78,6 +78,7 @@ npm run smoke:launch-persona npm run test:readiness npm run test:rls npm run audit:runtime +npm run security:repo npm run check:api npm run check:worker npm run check:taro @@ -87,7 +88,7 @@ node scripts/taro-h5-release-guardrails-test.js --require-dist npm run smoke:launch-persona ``` -`smoke:taro:h5` 会启动临时静态服务器和 mock API,验证三套 H5 的 `index.html`、JS/CSS 资源、history fallback、公开 runtime config 和 `/api/tenant/resolve` 契约。`smoke:taro:h5:interaction` 会在真实 Chrome/Edge 中点击学生、租户后台、平台后台关键路径,覆盖静态烟测发现不了的 JS 运行时、直接 history 路由刷新和 Taro 点击事件问题;当前脚本覆盖 25 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心,租户后台六个主模块,以及平台后台四个主模块。`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id`、`x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning;正式部署目录必须补齐。 +`smoke:taro:h5` 会启动临时静态服务器和 mock API,验证三套 H5 的 `index.html`、JS/CSS 资源、history fallback、公开 runtime config 和 `/api/tenant/resolve` 契约。`smoke:taro:h5:interaction` 会在真实 Chrome/Edge 中点击学生、租户后台、平台后台关键路径,覆盖静态烟测发现不了的 JS 运行时、直接 history 路由刷新和 Taro 点击事件问题;当前脚本覆盖 26 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块。`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id`、`x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning;正式部署目录必须补齐。 写入生产上线证据时,三套正式发布目录必须先放入真实公开 `runtime-config.json`,再运行严格模式: @@ -97,7 +98,7 @@ npm --silent run smoke:taro:h5:interaction -- --json > docs/refactor/launch-arti node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json > docs/refactor/launch-artifacts/taro-h5-release-guardrails.json ``` -如 `@codex-security` 插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。 +`security:repo` 是仓库自带的静态安全扫描,会拦截密钥形态、前端旧鉴权头、真实 runtime-config 和生产证据误入 Git。它不能替代真实 `@codex-security`;如插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。 生产 API 推荐: diff --git a/package.json b/package.json index fb94ea91..ceea9945 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "check:refactor": "npm run check:api && npm run check:worker && npm run check:importer && npm run pb:import:validate && npm run test:readiness && npm run test:pb:dry-run && npm run test:api", "audit:runtime": "npm audit --omit=dev --audit-level=high", "audit:taro:toolchain": "npm audit --workspace @tiku-saas/taro --audit-level=high", + "security:repo": "node scripts/repo-security-scan.js", "docker:api:build": "docker compose -f docker-compose.api.yml build", "docker:api:up": "docker compose -f docker-compose.api.yml up api", "docker:api:down": "docker compose -f docker-compose.api.yml down", @@ -57,7 +58,7 @@ "test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js", "test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js", "test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js", - "test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js", + "test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js", "test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js", "test:launch-gate": "node scripts/production-launch-gate-test.js", "smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js", diff --git a/scripts/production-launch-gate-test.js b/scripts/production-launch-gate-test.js index 3cb0d606..246fa289 100644 --- a/scripts/production-launch-gate-test.js +++ b/scripts/production-launch-gate-test.js @@ -153,6 +153,29 @@ assert.ok( 'slow API benchmark should be reported as a blocker', ); +const missingMixedBenchmark = runGate(tempDir => { + const evidence = createEvidence(tempDir); + evidence.checks = evidence.checks.filter(item => item.id !== 'performance.api-real-data-mixed'); + return evidence; +}); +assert.notEqual(missingMixedBenchmark.status, 0, 'missing mixed read/write benchmark should fail launch gate'); +assert.ok( + missingMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed' && item.status === 'blocker'), + 'missing mixed benchmark should be reported as a blocker', +); + +const slowMixedBenchmark = runGate(tempDir => { + const evidence = createEvidence(tempDir); + const item = evidence.checks.find(check => check.id === 'performance.api-real-data-mixed'); + item.summary.p95Ms = 501; + return evidence; +}); +assert.notEqual(slowMixedBenchmark.status, 0, 'slow mixed API benchmark should fail launch gate'); +assert.ok( + slowMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed.summary' && item.status === 'blocker'), + 'slow mixed benchmark should be reported as a blocker', +); + const missingBusinessSampling = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.checks = evidence.checks.filter(item => item.id !== 'migration.pb-import-sample'); @@ -186,6 +209,17 @@ assert.ok( 'missing H5 interaction smoke should be reported as a blocker', ); +const missingRepoSecurityScan = runGate(tempDir => { + const evidence = createEvidence(tempDir); + evidence.checks = evidence.checks.filter(item => item.id !== 'security.repo-scan'); + return evidence; +}); +assert.notEqual(missingRepoSecurityScan.status, 0, 'missing repository security scan should fail launch gate'); +assert.ok( + missingRepoSecurityScan.payload.checks?.some(item => item.id === 'check.security.repo-scan' && item.status === 'blocker'), + 'missing repository security scan should be reported as a blocker', +); + const missingAttestation = runGate(tempDir => { const evidence = createEvidence(tempDir); evidence.attestations = evidence.attestations.filter(item => item.id !== 'backup.snapshot'); diff --git a/scripts/production-launch-gate.js b/scripts/production-launch-gate.js index f352c3a1..71c0c725 100644 --- a/scripts/production-launch-gate.js +++ b/scripts/production-launch-gate.js @@ -82,6 +82,20 @@ const gateChecks = [ includeWrites: false, }, }, + { + id: 'performance.api-real-data-mixed', + label: 'Real-data API mixed read/write benchmark', + commandIncludes: 'perf:api:local', + summary: { + errors: 0, + errorRate: { lte: 0.001 }, + p95Ms: { lte: 500 }, + p99Ms: { lte: 1200 }, + concurrency: { gte: 50 }, + durationSeconds: { gte: 60 }, + includeWrites: true, + }, + }, { id: 'api.integration', label: 'API integration regression', @@ -152,10 +166,12 @@ const gateChecks = [ commandIncludes: 'smoke:taro:h5:interaction', summary: { fail: 0, - pass: { gte: 10 }, + pass: { gte: 26 }, 'mockApi.keyRequests.answers': { gte: 1 }, 'mockApi.keyRequests.favorites': { gte: 1 }, - 'mockApi.keyRequests.tenantResolve': { gte: 3 }, + 'mockApi.keyRequests.ordersCreated': { gte: 1 }, + 'mockApi.keyRequests.paymentsCreated': { gte: 1 }, + 'mockApi.keyRequests.orderStatus': { gte: 1 }, }, }, { @@ -173,6 +189,12 @@ const gateChecks = [ commandIncludes: 'audit:runtime', summary: { critical: 0, high: 0 }, }, + { + id: 'security.repo-scan', + label: 'Repository static security scan', + commandIncludes: 'security:repo', + summary: { critical: 0, high: 0 }, + }, { id: 'security.codex-scan', label: 'Codex Security scan', diff --git a/scripts/repo-security-scan-test.js b/scripts/repo-security-scan-test.js new file mode 100644 index 00000000..4dbdaa2a --- /dev/null +++ b/scripts/repo-security-scan-test.js @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const repoRoot = process.cwd(); +const scriptPath = path.join(repoRoot, 'scripts', 'repo-security-scan.js'); + +function run(cwd) { + return spawnSync(process.execPath, [scriptPath, '--json'], { + cwd, + encoding: 'utf8', + env: { + PATH: process.env.PATH || '', + Path: process.env.Path || '', + SystemRoot: process.env.SystemRoot || '', + ComSpec: process.env.ComSpec || '', + TEMP: process.env.TEMP || os.tmpdir(), + TMP: process.env.TMP || os.tmpdir(), + }, + }); +} + +const clean = run(repoRoot); +assert.equal(clean.status, 0, `current repository should pass security scan: ${clean.stdout} ${clean.stderr}`); +const cleanPayload = JSON.parse(clean.stdout); +assert.equal(cleanPayload.summary.critical, 0); +assert.equal(cleanPayload.summary.high, 0); + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-security-scan-')); +try { + fs.mkdirSync(path.join(tempDir, 'apps', 'taro', 'src'), { recursive: true }); + fs.writeFileSync( + path.join(tempDir, 'apps', 'taro', 'src', 'unsafe.ts'), + "export const headers = { 'x-user-id': '123' };\n", + 'utf8', + ); + fs.writeFileSync( + path.join(tempDir, 'secret.env'), + [ + 'DATABASE_URL=postgresql://postgres:real-password@db.example.com:5432/postgres', + 'SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake-service-role-token-that-should-not-ship', + '', + ].join('\n'), + 'utf8', + ); + + const unsafe = run(tempDir); + assert.notEqual(unsafe.status, 0, 'unsafe fixture should fail security scan'); + const payload = JSON.parse(unsafe.stdout); + assert.ok(payload.findings.some(item => item.id === 'frontend-legacy-user-header'), 'x-user-id should be detected'); + assert.ok(payload.findings.some(item => item.id === 'postgres-url'), 'database URL should be detected'); + assert.ok(payload.findings.some(item => item.id === 'supabase-service-role'), 'service role key should be detected'); +} finally { + fs.rmSync(tempDir, { recursive: true, force: true }); +} + +console.log('[PASS] repository security scan'); diff --git a/scripts/repo-security-scan.js b/scripts/repo-security-scan.js new file mode 100644 index 00000000..5c0bad34 --- /dev/null +++ b/scripts/repo-security-scan.js @@ -0,0 +1,261 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const repoRoot = process.cwd(); + +const textExtensions = new Set([ + '.js', + '.jsx', + '.ts', + '.tsx', + '.json', + '.md', + '.sql', + '.toml', + '.yml', + '.yaml', + '.html', + '.css', + '.scss', + '.env', + '.example', +]); + +const ignoredSegments = new Set([ + '.git', + 'node_modules', + 'dist', + '.swc', + '.temp', + '.branches', + 'launch-artifacts', + 'performance-reports', + 'migration-reports', + 'pb_export', + 'pb_data', + 'pb_public', + '参考', + '新UI参考', + 'sao_live_0630', + 'whisper_models', +]); + +const allowlistedFiles = new Set([ + '.env.example', + 'README.md', + 'docs/refactor/ai-development-guardrails.md', + 'docs/refactor/api-structure.md', + 'docs/refactor/auth-payment-provider-plan.md', + 'docs/refactor/frontend-handoff-index.md', + 'docs/refactor/multitenant-auth-security-contract.md', + 'docs/refactor/object-storage-production-runbook.md', + 'docs/refactor/object-storage.md', + 'docs/refactor/production-launch-evidence.template.json', + 'docs/refactor/supabase-frontend-access-strategy.md', + 'docs/refactor/taro-frontend-integration.md', + 'docs/refactor/taro-h5-deployment.md', + 'docs/refactor/taro-production-integration-checklist.md', + 'docs/refactor/web-launch-acceptance-checklist.md', + 'scripts/production-readiness-check-test.js', + 'scripts/repo-security-scan.js', + 'scripts/taro-h5-release-guardrails-test.js', +]); + +const ruleAllowlistedFiles = { + 'postgres-url': new Set([ + 'scripts/production-config-failfast-test.js', + 'scripts/repo-security-scan-test.js', + ]), + 'supabase-service-role': new Set([ + 'scripts/repo-security-scan-test.js', + 'scripts/taro-runtime-config-test.js', + ]), + 'frontend-legacy-user-header': new Set([ + 'scripts/repo-security-scan-test.js', + ]), +}; + +const rules = [ + { + id: 'postgres-url', + severity: 'high', + pattern: /postgres(?:ql)?:\/\/[^\s"'`<>]+/i, + message: 'Database connection strings must not be committed outside approved examples/docs.', + validate: (match) => { + try { + const url = new URL(match[0]); + return !['127.0.0.1', 'localhost', 'host.docker.internal'].includes(url.hostname); + } catch { + return true; + } + }, + }, + { + id: 'private-key', + severity: 'critical', + pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/i, + message: 'Private keys must not be committed.', + }, + { + id: 'supabase-service-role', + severity: 'critical', + pattern: /\b(?:SUPABASE_SERVICE_ROLE_KEY|SUPABASE_SECRET_KEY)\s*[:=]\s*["']?([A-Za-z0-9._-]{20,})/i, + message: 'Supabase service role or secret keys must never enter the repository.', + validate: (match) => { + const value = String(match[1] || ''); + return /^eyJ/.test(value) || /^sb_secret_/.test(value) || value.length >= 80; + }, + }, + { + id: 'provider-secret-token', + severity: 'critical', + pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/, + message: 'Provider secret keys must not be committed.', + }, + { + id: 'cloud-access-key', + severity: 'critical', + pattern: /\bAKIA[0-9A-Z]{16}\b/, + message: 'Cloud access keys must not be committed.', + }, + { + id: 'wechat-pay-private-key', + severity: 'critical', + pattern: /\b(?:WECHAT_PAY_PRIVATE_KEY|ALIPAY_APP_PRIVATE_KEY)\s*[:=]\s*["']?(?!replace-|<|xxx|your-).{20,}/i, + message: 'Payment private keys must stay in backend secrets/KMS.', + }, + { + id: 'frontend-legacy-user-header', + severity: 'high', + pathPattern: /^apps\/taro\/src\//, + pattern: /\bx-user-id\b/i, + message: 'Taro source must not use x-user-id; identity comes from Supabase JWT/session.', + }, + { + id: 'frontend-platform-key', + severity: 'high', + pathPattern: /^apps\/taro\/src\//, + pattern: /\bx-platform-admin-key\b/i, + message: 'Taro source must not use platform admin key headers.', + }, + { + id: 'frontend-pocketbase', + severity: 'high', + pathPattern: /^apps\/taro\/src\//, + pattern: /\bpocketbase\b/i, + message: 'Taro source must not depend on PocketBase.', + }, + { + id: 'runtime-config-committed', + severity: 'high', + pathPattern: /(^|\/)runtime-config\.json$/, + pattern: /./, + message: 'Real H5 runtime-config.json files are deployment artifacts and must not be committed.', + }, + { + id: 'production-launch-evidence-committed', + severity: 'high', + pathPattern: /^docs\/refactor\/production-launch-evidence\.json$/, + pattern: /./, + message: 'Real production launch evidence may contain internal evidence paths and must not be committed.', + }, +]; + +function normalizeSlashes(value) { + return value.replace(/\\/g, '/'); +} + +function relative(filePath) { + return normalizeSlashes(path.relative(repoRoot, filePath)); +} + +function isIgnoredPath(filePath) { + const rel = relative(filePath); + return rel.split('/').some(segment => ignoredSegments.has(segment)); +} + +function shouldRead(filePath) { + if (isIgnoredPath(filePath)) return false; + const ext = path.extname(filePath).toLowerCase(); + if (textExtensions.has(ext)) return true; + return ['Dockerfile', '.gitignore', '.dockerignore'].includes(path.basename(filePath)); +} + +function walk(dir) { + const result = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (isIgnoredPath(fullPath)) continue; + if (entry.isDirectory()) result.push(...walk(fullPath)); + else if (shouldRead(fullPath)) result.push(fullPath); + } + return result; +} + +function getTrackedFiles() { + const result = spawnSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' }); + if (result.status !== 0) return null; + return new Set(result.stdout.split(/\r?\n/).filter(Boolean).map(normalizeSlashes)); +} + +function scanFile(filePath, trackedFiles) { + const rel = relative(filePath); + const text = fs.readFileSync(filePath, 'utf8'); + const findings = []; + const isTracked = !trackedFiles || trackedFiles.has(rel); + + for (const rule of rules) { + if (rule.pathPattern && !rule.pathPattern.test(rel)) continue; + if (!rule.pathPattern && allowlistedFiles.has(rel)) continue; + if (ruleAllowlistedFiles[rule.id]?.has(rel)) continue; + const match = text.match(rule.pattern); + if (!match) continue; + if (rule.validate && !rule.validate(match, { file: rel, text })) continue; + if ((rule.id === 'runtime-config-committed' || rule.id === 'production-launch-evidence-committed') && !isTracked) continue; + findings.push({ + id: rule.id, + severity: rule.severity, + file: rel, + message: rule.message, + }); + } + + return findings; +} + +function summarize(findings) { + return findings.reduce( + (summary, item) => { + summary[item.severity] = (summary[item.severity] || 0) + 1; + return summary; + }, + { critical: 0, high: 0, medium: 0, low: 0 }, + ); +} + +function main() { + const json = process.argv.includes('--json'); + const trackedFiles = getTrackedFiles(); + const files = walk(repoRoot); + const findings = files.flatMap(filePath => scanFile(filePath, trackedFiles)); + const summary = { + ...summarize(findings), + scannedFiles: files.length, + findings: findings.length, + }; + const payload = { summary, findings }; + + if (json) { + console.log(JSON.stringify(payload, null, 2)); + } else { + console.log(`Repository security scan: ${summary.findings} finding(s), ${summary.scannedFiles} file(s) scanned`); + for (const item of findings) { + console.log(`[${item.severity.toUpperCase()}] ${item.id} ${item.file}: ${item.message}`); + } + } + + if (summary.critical > 0 || summary.high > 0) process.exitCode = 1; +} + +main(); diff --git a/scripts/taro-h5-interaction-smoke.js b/scripts/taro-h5-interaction-smoke.js index 646e31c7..2820e61c 100644 --- a/scripts/taro-h5-interaction-smoke.js +++ b/scripts/taro-h5-interaction-smoke.js @@ -1015,6 +1015,15 @@ async function runStudentJourney(browser, portal, api) { await waitForText(page, '会员收银台'); checks.push({ id: 'student.profile.to_checkout', status: 'pass', detail: await currentPath(page) }); + await clickText(page, '提交订单'); + await waitForApiRequest(api, '/api/commerce/orders', 'POST'); + await waitForApiRequest(api, '/api/commerce/payments/create', 'POST'); + await waitForText(page, '支付参数已生成'); + await waitForText(page, 'SMOKE202607010001'); + await clickText(page, '刷新状态'); + await waitForApiRequest(api, '/api/commerce/orders/status', 'GET'); + checks.push({ id: 'student.checkout.order_payment', status: 'pass', detail: '收银台下单、支付参数生成和状态刷新 API 已触发' }); + await navigateAndExpect(page, portal.baseUrl, '/pages/student/review/index?type=wrong', '错题本'); await waitForText(page, '开始复习'); await waitForText(page, '这是 H5 交互烟测题目'); @@ -1213,6 +1222,9 @@ async function main() { keyRequests: { answers: api.requests.filter(item => item.path === '/api/learning/answers').length, favorites: api.requests.filter(item => item.path === '/api/learning/favorites/questions').length, + ordersCreated: api.requests.filter(item => item.path === '/api/commerce/orders' && item.method === 'POST').length, + paymentsCreated: api.requests.filter(item => item.path === '/api/commerce/payments/create' && item.method === 'POST').length, + orderStatus: api.requests.filter(item => item.path === '/api/commerce/orders/status' && item.method === 'GET').length, tenantResolve: api.requests.filter(item => item.path === '/api/tenant/resolve').length, }, },