From e11ce1aaaa602011718e300e4527a0d971f1339a Mon Sep 17 00:00:00 2001 From: malei <> Date: Sat, 1 Aug 2026 23:30:32 +0800 Subject: [PATCH] chore: guard frontend production builds --- .env.mp-weixin | 6 ++-- .env.production | 7 +++-- README.md | 16 +++++++--- package.json | 5 +-- scripts/release-preflight.cjs | 59 +++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 scripts/release-preflight.cjs diff --git a/.env.mp-weixin b/.env.mp-weixin index c5f6da3..1c9dfaf 100644 --- a/.env.mp-weixin +++ b/.env.mp-weixin @@ -1,3 +1,3 @@ -# 小程序构建时的 API 域名(api.s-good.com 已挪作他用,勿再指向) -# 提审/体验版前请改为宠小它专用域名,并在微信后台配置合法域名 -VITE_API_ORIGIN=http://localhost:8080 +# 安全占位符:提审/体验版构建前必须用受控环境文件覆盖,并运行发布门禁。 +VITE_API_ORIGIN=https://api.petstore.invalid +VITE_REPORT_PUBLIC_ORIGIN=https://report.petstore.invalid diff --git a/.env.production b/.env.production index c0479dc..88001e1 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,4 @@ -# 宠小它生产 API 域名(api.s-good.com 已挪作他用,勿再指向) -# 部署前请改为宠小它专用域名,例如 https://api.chongxiaota.example -VITE_API_ORIGIN=http://localhost:8080 +# 安全占位符:正式构建必须通过 PETSTORE_FRONTEND_ENV_FILE 指向服务器/CI 的受控环境文件, +# 并先执行 npm run preflight:release;.invalid 会被门禁拒绝。 +VITE_API_ORIGIN=https://api.petstore.invalid +VITE_REPORT_PUBLIC_ORIGIN=https://report.petstore.invalid diff --git a/README.md b/README.md index 0161e32..e0249b6 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,7 @@ npm run dev:h5 开发阶段:在微信开发者工具中勾选「不校验合法域名」(设置 → 项目设置) -生产阶段:在微信公众平台后台添加以下合法域名: -- `http://localhost:8080`(开发用) -- 你的实际后端域名 +生产阶段:仅在微信公众平台后台添加本项目实际 HTTPS 后端域名;localhost 只用于开发,不能进入体验版/正式版。 ## API 配置 @@ -147,15 +145,23 @@ npm run dev:h5 # 1. 安装依赖 npm --prefix frontend install -# 2. H5 构建 +# 2. 用权限受控的真实生产环境文件做门禁(不会打印变量值) +PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-h5.env npm --prefix frontend run preflight:release + +# 3. 让 Vite 使用同一份已通过门禁的配置并构建 H5 +cp /secure/path/petstore-h5.env frontend/.env.production.local npm --prefix frontend run build:h5 # 期望:DONE Build complete. -# 3. 微信小程序构建 +# 4. 小程序配置单独门禁并构建 +PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-mp.env npm --prefix frontend run preflight:release +cp /secure/path/petstore-mp.env frontend/.env.mp-weixin.local npm --prefix frontend run build:mp-weixin # 期望:DONE Build complete. + [verify-mp-app-json] OK ``` +仓库内 `.env.production` / `.env.mp-weixin` 只含 `.invalid` 安全占位符,会被门禁主动拒绝。真实域名文件不得提交;发布结束后删除临时 `.env.*.local`。 + ### 前端 RC 检查项 - [ ] `VITE_API_ORIGIN` 已配置(宠小它专用后端 API 域名;**勿再用** `api.s-good.com`) diff --git a/package.json b/package.json index ad87c80..32935b8 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "private": true, "scripts": { "dev:mp-weixin": "uni -p mp-weixin", - "build:mp-weixin": "uni build -p mp-weixin && node scripts/verify-mp-app-json.cjs", + "build:mp-weixin": "uni build -p mp-weixin --mode mp-weixin && node scripts/verify-mp-app-json.cjs", "dev:h5": "uni", - "build:h5": "uni build" + "build:h5": "uni build", + "preflight:release": "node scripts/release-preflight.cjs" }, "dependencies": { "@dcloudio/uni-app": "3.0.0-4060620250520001", diff --git a/scripts/release-preflight.cjs b/scripts/release-preflight.cjs new file mode 100644 index 0000000..2592b4e --- /dev/null +++ b/scripts/release-preflight.cjs @@ -0,0 +1,59 @@ +const fs = require('node:fs') +const path = require('node:path') + +const envFile = process.env.PETSTORE_FRONTEND_ENV_FILE || '.env.production' +const absoluteEnvFile = path.resolve(process.cwd(), envFile) + +function fail(message) { + console.error(`[release-preflight] FAIL: ${message}`) + process.exit(1) +} + +if (!fs.existsSync(absoluteEnvFile)) { + fail('environment file does not exist') +} + +const values = {} +for (const rawLine of fs.readFileSync(absoluteEnvFile, 'utf8').split(/\r?\n/)) { + const line = rawLine.trim() + if (!line || line.startsWith('#')) continue + const separator = line.indexOf('=') + if (separator < 1) continue + const key = line.slice(0, separator).trim().replace(/^export\s+/, '') + const value = line.slice(separator + 1).trim().replace(/^(['"])(.*)\1$/, '$2') + values[key] = value +} + +function requireProductionOrigin(name, forbiddenHosts) { + const value = values[name] + if (!value) fail(`${name} is required`) + + let url + try { + url = new URL(value) + } catch { + fail(`${name} must be a valid URL`) + } + + const hostname = url.hostname.toLowerCase() + const isLocal = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' + const isPlaceholder = hostname.endsWith('.invalid') + || hostname.endsWith('.test') + || hostname.endsWith('.example') + || ['example.com', 'example.net', 'example.org'].some( + (reserved) => hostname === reserved || hostname.endsWith(`.${reserved}`), + ) + const hasExtraParts = Boolean(url.username || url.password || url.search || url.hash) + || (url.pathname !== '' && url.pathname !== '/') + if (url.protocol !== 'https:' || isLocal || isPlaceholder || hasExtraParts) { + fail(`${name} must be an explicit production HTTPS origin`) + } + if (forbiddenHosts.includes(hostname)) { + fail(`${name} uses a domain reserved by another product`) + } + console.log(`[release-preflight] ${name}: PASS`) +} + +requireProductionOrigin('VITE_API_ORIGIN', ['api.s-good.com']) +requireProductionOrigin('VITE_REPORT_PUBLIC_ORIGIN', ['www.s-good.com']) +console.log('[release-preflight] frontend release configuration PASS')