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

6.6 KiB
Raw Permalink Blame History

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 索引)。