Files
crearte-monorepo/docs/specs/2026-09-30-hosted-submission-design.md

73 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P6 hosted 作品投稿 · 设计 spec(方案 A:自托管内嵌)
日期:2026-09-30 | 决策人:owner(选 A)| 前置勘查:控制者本会话完成,证据均带 file:line。
## 0. 一句话
作者把游戏部署在别处(itch.io / 自建站),投稿填一个 https URL,平台站内用沙箱 iframe 播放;打通的是「入口」,播放机件早已全存在。
## 1. 现状勘查(grounded)
**后端(crearte-server)hosted 半边已通:**
- `works` 表(migration 0002)含 `runtime IN ('external','virtual','hosted')`、`hosted_url`(CHECK https)、`play_origin`、`fallback IN ('external','hosted','none')`、`entry`。
- 校验已认:`service/validate.go:133-141` —— `hosted` 合法 runtime,且要求 `HostedURL` 匹配 `payloadHTTPSPattern`。
- **落库映射不缺**:`service/import.go:39 PayloadToWork` 已映射 `PlayOrigin/HostedURL/Fallback/Entry`(此前「Approve 落库缺口」系误报——grep 打在 content.go 而映射在 import.go);`repository/work.go:73/96/109` Create/Upsert/UpdateMetadata 全列含 `hosted_url`。
- 提交创建 `content.go:327-358`:virtual 必须有 bundle;hosted 无 bundle 要求但也**未拒绝**携带 bundle;metadata_change 对 runtime **无不可变性校验**(作者可借元数据变更改 runtime)。
- `PlaySubdomain(workID)`(`namespace.go:16`)确定性派生 sha256 前 8 字节 hex,对全部 runtime 生效;hosted 不需要平台签发子域(播放源即 hostedUrl),playOrigin 可显式覆盖(CONTRIBUTING:42 已写明)。
**前端(crearte)播放端全存在、入口焊死:**
- `GameView.vue:30`:`playable = virtual || hosted`;`runtime/host/GameHost.vue:98-101`:iframe `:src="iframeSrc"`、`:sandbox`、`:allow`、`referrerpolicy="no-referrer"`、降级外链按钮(`:108` `degradedToExternal`)。
- `runtime/host/useGameFrame.ts:58/159/162`:hosted 目标直接置 `ready`(不等 agent:boot),3 秒未收到 `agent:boot` 仅 console warn(桥不存在属预期)。
- `types.ts:16`:`GameRuntimeMode` 三值齐全;但 `SubmitFormView.vue:33` 表单 runtime 仅 `external|virtual`,`:144-147` 对 hosted 作品预填**直接拒绝**(防元数据变更静默降级——该风险根源在后端缺 D-B,见下)。
- 内容层提交 payload 的 FE 类型同样只两档(SubmitFormView:144 注释)。
**deploy / CSP:**
- `sw/csp.ts:29`、`serve-runtime.mjs:41` 的 `frame-src 'none'` 是**运行时子域页面自身**的 CSP(管 play 页里再嵌 iframe),不是主站宿主页面——hosted iframe 挂在主站页面,不受此约束。
- deploy 仓 grep 无主站 CSP frame-src 下发(nginx 模板未含);验收腿 T5 实核生产响应头。
**结论**:方案 A 的真实工作量 = 后端两处校验收紧 + 前端表单第三档 + 预填解锁 + 测试与验收,播放链路零改动。
## 2. 决策(D-A…D-F)
- **D-A hostedUrl 政策**:必须 https(已有校验复用);**任意域名**、不做白名单——第三方禁嵌(X-Frame-Options/CSP frame-ancestors)平台不可预判,由播放端既有 fallback 链兜底(降级外链)。iframe 安全参数沿用现状(sandbox 旗标集 + no-referrer + 外链 `rel=noopener noreferrer`),不扩权。
- **D-B runtime 不可变**(根治,后端):`metadata_change` 提交的 `payload.runtime`(经 `RuntimeOrDefault`)与作品现存 runtime 不一致 → `ValidationError{Fields:["runtime: immutable on metadata_change"]}`。此后前端 `:144-147` 拒绝分支删除(表单能表达 hosted,预填安全)。收紧只作用于**新提交**:审批路径不复跑 ValidateSubmission(Approve 只做 ParseEnvelope+落库,`content.go:596+`),存量 draft 无 retroactive 失效。
- **D-C hosted 语义纯度**:`runtime=hosted` 且带 `bundle_upload_id` → `ValidationError`(hosted 不吃平台文件);`features` 允许提交但桥不存在(armBridgeWarn 已容忍);`entry`/`playSubdomain` 对 hosted 无意义,存而不使用,不报错。cover 上传与 runtime 无关,照常允许。
- **D-D 审核面最小改动**:AdminView 待审卡片对 hosted 提交展示 `hostedUrl` 文本行(不内嵌播放预览——审核者安全 + 最小变更)。
- **D-E fallback**:复用现有列;表单在 hosted 档下提供 fallback 选择(`external|hosted|none`),默认引导 `external`(需 url 字段配合,表单校验:fallback=external 时 url 必填)。
- **D-F deploy 仅核证**:实探主站 CSP 头;无 frame 限制则零改动记录在案,有则补模板并复验(T5)。
## 3. 范围
**server(→0.13.0)**
1. D-B:metadata_change runtime 不可变校验 + 单测。
2. D-C:hosted 禁 bundle 校验 + 单测。
3. 集成(`-count=1` 空库腿):hosted new_work 全路径 create→approve→`works.hosted_url/play_origin/fallback` 落库读回;GET 公开作品含 `runtime=hosted` + `hostedUrl`。
4. CHANGELOG 中英成对。
**crearte(→0.19.0)**
1. FE 内容层提交 payload 类型三档 + `hostedUrl`/`fallback` 字段。
2. SubmitFormView:第三 radio「自托管内嵌」+ hostedUrl 输入 + fallback 选择;校验(https、hosted 必填 hostedUrl、fallback=external 时 url 必填);预填解除 `:144-147` 拒绝、正确回填 hosted 字段。
3. vitest:表单 hosted 分支(提交形状 / 校验失败面 / 预填回填)。
4. e2e:admin-flow 夹具补 hosted 投稿流(表单提交形状断言);主套件全绿。
5. CHANGELOG 中英成对。
**deploy**:仅 T5 核证,预期零改动。
**不做**(明确排除):URL 抓取/健康探测、域名信任列表、hosted 的版本更新(new_version 仍 virtual-only)、转码代理、离线缓存、播放端任何改动。
## 4. 红线
- 不动 `full-loop.spec.ts` / `playwright.stack.config.ts` / 运行时三件套(sw/agent/bootstrap)。
- 不动生产凭据与 `.env`;deploy 改动(如有)只出模板级。
- server 迁移零新增(hosted 列 0002 起就在)。
- 前端不新增后端端点依赖;管理面仅 D-D 一行展示。
## 5. 验收(控制者本机,同 P7 六腿口径)
1. server 容器四连(gofmt/vet/build/test)RC=0。
2. fresh 空库 `-count=1` 集成全过、零 FAIL 零静默 skip。
3. curl 冒烟真栈:hosted 草稿→submit→approve→DB 行 `hosted_url` 非空、`GET /api/games/<id>` 含 runtime/hostedUrl;负面:runtime 篡改元数据变更 400、hosted 带 bundle 400。
4. FE:vitest 全量 / typecheck / admin-flow / 主套件 RC=0。
5. 真栈 full-loop 若分支后端被触及照跑 1 passed。
6. 双路独立审查(spec→代码逐条)PASS 后合主,wrapper 记账(ROADMAP P6 行、CHANGELOG 0.2.7、spec/plan 索引)。