test(游戏共创): 一致性门禁补「路径形状」维度,并同步校验器文档

- `scripts/check-project-bundle-policy-parity.mjs` 新增第 3 维:服务端 `normalize_archive_path` 的拒绝形状必须被客户端打包器的三个常量逐 token 覆盖——**哨兵段**(`.` / `..`)与**路径段结尾字符**(空格 / 点)分别用 `rustStringArray` / `rustCharArray` 抽取,**路径段禁止字符**同时抓 `.contains('x')` 与 `matches!(expr, 'a' | 'b')` 两种写法;再单独钉「两侧都必须显式拒 `/` 开头与 `\`」两条独立分支(它们靠常量表覆盖不到)。**空集合一律 throw**,沿用既有「抽不到不许当绿灯」的写法
- 修掉我自己在第一版里写错的两处(由补强过程中的实测发现,并已用临时副本验证修法):① `BUNDLE_FORBIDDEN_PATH_SEGMENTS` 是 `&[&str]` 而非 `&[char]`,须用 `rustStringArray`(`".."` 也无法用 char 字面量表达);② 反斜杠分支的正则写成了 4 个字面反斜杠,而源码是 `contains('\\')`(2 个),已改为 `/contains\('\\\\'\)/u`
- **「故意破坏会红」已验证**:临时从客户端 `BUNDLE_FORBIDDEN_PATH_CHARS` 去掉 `'?'` 与 `'*'` → 门禁立即报两条「路径段禁止字符:服务端有「?」/「*」,客户端没有」,随后已原样恢复
- 文档 `§3.5.2/§3.2.3`:校验器清单同步到新规则(新增目录/前缀/后缀/全名 + 内容嗅探及其取舍理由、嵌套包扩展名并集与 magic 嗅探、读取层封顶与压缩比口径更正、路径形状维度),并新增**产品口径「知情同意」**一条——清单是黑名单,`docs/`、`*.pdf`、`notes.txt`、截图等不在任何拒绝集内会原样外发,发布面板必须写明「整个项目目录(除少数排除项)会原样公开」
- 门禁:policy parity 0(服务端 51 条全被客户端覆盖 + 路径形状维度 OK);`check:doc-index` 248 份 OK;`check:encoding` OK;`git diff --check` 0
This commit is contained in:
2026-10-05 16:54:14 +08:00
parent 10d1fcbc5f
commit 88368d0418
2 changed files with 90 additions and 1 deletions
@@ -279,6 +279,8 @@ pub(crate) project_bundle_sha256: Option<String>,
对象键:`{GAME_DISTRIBUTION_OBJECT_PREFIX}{game_id}/{version_id}.project.zip`(即 `agc/project-snapshots/v1/game-distribution/{game_id}/{version_id}.project.zip`)——与发行包同前缀族便于生命周期统一,靠 `.project.zip` 后缀区分资产,避免同一(作品, 版本)的两份资产互相覆盖或串用读取缓存。对象键只在服务端使用,投影里只回 `projectBundleBytes` / `projectBundleSha256`。
- **知情同意(2026-10-05 复核后新增,必须落到面板文案)**:校验清单是**黑名单**——作者工程里的 `docs/`、`*.pdf`、`notes.txt`、截图、素材源文件等**不在任何拒绝集内,会被原样外发**。因此发布面板必须写明「**整个项目目录(除少数排除项)会原样公开**」,让作者自己决定要不要把某个文件留在工程里。这不是技术漏洞,而是知情同意要求;技术侧能保证的是「凭据类内容尽量被拦」,不能保证「除源码外什么都不外发」。
### 3.3 状态与流转
| 对象 | 字段 | 取值 | 合法迁移 | 触发方 | 并发控制 |
@@ -358,7 +360,12 @@ pub(crate) project_bundle_sha256: Option<String>,
- **打包(AGC,已落地)**:客户端 `project_bundle` 模块做**确定性打包**(条目排序 + 固定时间戳,同内容同摘要),排除 `.agent` / `.git` / `.svn` / `node_modules` 段(任意层级)与根 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode`;客户端自身另有总量上限(512 MiB)。
- **服务端复核(已落地,**不信任客户端**)**:`module-game-distribution/src/project_bundle.rs` 的 `validate_project_bundle_zip` 独立校验上传内容——上限与发行包**逐项相等**(压缩包 200 MiB / 展开 500 MiB / 单文件 64 MiB / 条目 10 000 / 压缩比 100,理由是两者共用同一条上传链路,反代与 Pingora 的放行量就是按 200 MiB 校准的);额外拦凭据与隐私文件(`.env`、`.env*`、`*.pem`、`*.key`、`*.p12`、`*.pfx`、`.npmrc`、`.netrc`、`.git-credentials`、`id_rsa*`、`id_ed25519*`、`*.map`)、嵌套 `.zip`、符号链接、加密条目与不安全路径;**不要求**根 `index.html`(源码包没有入口约定)。
- **上传(AGC,已落地)**:发布链路在「授权非 `forbidden`」时随版本上传工程源包(失败**不阻断**发布,按可重试处理);面板明确提示「未上传工程包,你的作品只能被他人试玩与参考(不能直接重新发布)」。是否有工程包**不改变**授权是否开放(见 §3.2.3 的产品口径)。客户端规则与服务端对齐,并有机器门禁 `npm run check-project-bundle-policy-parity` 钉住两端排除清单/上限一致。
- **上传(AGC,已落地)**:发布链路在「授权非 `forbidden`」时随版本上传工程源包(失败**不阻断**发布,按可重试处理);面板明确提示「未上传工程包,你的作品只能被他人试玩与参考(不能直接重新发布)」。是否有工程包**不改变**授权是否开放(见 §3.2.3 的产品口径)。客户端规则与服务端对齐,并有机器门禁 `npm run check-project-bundle-policy-parity` 钉住两端排除清单/上限一致(含路径形状维度,见下)。
- **凭据/依赖识别补强(2026-10-05 对抗性复核后)**:除上面那份清单,还拦任意层级的 `.aws` / `.ssh` / `.kube` / `.docker` / `.gnupg` / `.terraform` / `.secrets`,以及文件名前缀 `credentials` / `id_ecdsa` / `id_dsa` / `terraform.tfstate` / `service-account`、后缀 `.jks` / `.keystore` / `.ppk` / `.p8` / `.kdbx` / `.der`、全名 `.htpasswd` / `.pgpass`。另外对**小体积文本条目**做内容特征扫描(PEM 私钥块标记、AWS `AKIA…`、GitHub `ghp_` / `github_pat_`、Slack `xox*`)——这是唯一能兜住「未知凭据文件名」的形状;更宽的前缀启发(例如 `sk-`)**故意不做**,因为误报会直接阻断作者发布,收益与代价不成比例。
- **嵌套包识别**:扩展名并集 `.zip` / `.tar` / `.gz` / `.tgz` / `.7z` / `.rar` / `.jar` / `.whl` / `.nupkg`,**并对每个条目按 magic bytes 嗅探**(`PK\x03\x04`、`7z`、`Rar!`、gzip、tar 的 `ustar`)——改名成 `.dat` 也拦。客户端打包器同款检查,命中即报错(指明路径与格式),不静默跳过。
- **规模检查的读取层封顶**:实际读取按**该条目声明大小**封顶(只允许多读 1 字节用于判溢出,短读同样算失败),预分配同样收紧到硬上限。因此「声明说谎的 deflate 炸弹」在**读**这一层就失败关闭,不会先解完再比对;**同一处修法同步到发行包校验器**(那里原有同样的写法)。
- **压缩比口径更正**:分母是**该条目的压缩字节**(不是整包字节),而且它**不是 zip 炸弹防线**——真正的约束是单文件 64 MiB 与累计展开 500 MiB 两条上限,压缩比只是补充。文档与代码注释都按这个语义写。
- **路径形状维度(一致性门禁新增)**:客户端过去只过 `template_library::safe_archive_relative_path`(不查结尾点/空格与 `<>"|?*`),于是 macOS/Linux 上 `src/a?.ts`、`x.` 这类名字**客户端能打、服务端必 422**(白传一趟)。现在客户端打包器自带一份与服务端逐字符对齐的路径形状检查,且 `check-project-bundle-policy-parity` 新增该维度(服务端的哨兵段 / 结尾字符 / 禁止字符必须被客户端的常量逐 token 覆盖,抽不到即报错)。
- **下载与建项(AGC,已落地)**:`fork-source` 返回 `source: 'project'` 时走 `GET /games/{gameId}/fork-source/project`(`package` 时走原路径)→ 校验 `sha256` 与 `bytes` → 解压到 `<app_data>/forks/<gameId>/<versionId>/`(与模板安装同一套路径门禁与私有 DACL 写入)→ 按来源分支建项(源码形态可直接改源码/重跑构建;成品包形态是可玩参考副本)→ 写 `manifest.forkedFrom`。来源记录 `.agent/fork-source.json` 升到 **v2**(记录取件形态与对应摘要),**兼容 v1**(旧记录仍可读);取件成功提示按两种形态分别如实措辞。
- **优先级(已落地)**:同一版本同时存在工程源包与成品包时,`fork-source` 优先返回工程源包并标 `source: 'project'`;只有工程包缺失(或行上「字节数 > 0 但摘要为空」这种半写状态)才回落 `package`,客户端据此决定建项形态。