Uh oh!
There was an error while loading. Please reload this page.
fix(console): PublicFormsPage 的 redirect 授权门改问契约,越契约值在保存时刻被拒 (#4990) - #5008
Conversation
objectstack#7496(2026-08-11 裁定,objectstack#7657 落 spec,本仓 17.0.0 GA pin 已覆盖)把 `submitBehavior.url` 裁为仅相对路径,spec 对七族越契约值各给一句 author-facing 处方。PublicFormsPage 的 redirect 分支只执行了其中第一族(非空), 其余六族原样写进 view 元数据 —— 管理员输入 `https://example.com/thanks` 或 `javascript:alert(1)` 得不到任何提示,而这个界面自己刚教过他这个值可以用:字段是 `type="url"`(它自己认的「合法」恰是本键拒绝的绝对 URL),占位符是 `https://example.com/thanks`。 - 保存时刻拒绝,文案来自 spec。判定复用 #4992 落地的同一条 `FormViewSchema` 解析路径:`submitRedirect` 新导出 `checkSubmitRedirectUrl`,消费侧 `resolveSubmitRedirect` 与授权门共用它。绝对 URL、script/data scheme、 协议相对 `//host`、反斜杠、空白与控制字符、畸形 `{{record.field_name}}` token、 文档相对路径、空值各自得到 spec 自己那句处方,显示在字段旁。 规则不在此处重写一份:镜像拷贝在 spec 改动前的每次取值比较里都是绿的,正是 `scripts/check-spec-symbol-derivation.mjs` 要劝退的形状;裁定日后放宽,跟着 pin 走而不需要改这里。 - 本地的 `Redirect URL is required` 去掉。空值本就是七族之一,一并走契约,作者 读到的是「目的地长什么样」而不是「必填」。 - 字段不再教错值:改回纯文本输入,占位符 `/thanks`,并给出规则提示(应用内路径、 `{{record.field_name}}` 插值、故意外跳请用 app navigation item)。 保存的是 schema 接受后回读的值,所以授权门与渲染侧不可能对同一个目的地有两种 意见。`thank-you` 的 `title`/`message` 刻意不校验:spec 里两者就是自由串,没有 可供门陈述的契约。 服务端已测(只读判定,未起服务):`saveMetaItem` 的 spec-conformance gate 走 `getMetadataTypeSchema('view')`(同一个 17.0.0 GA pin),对这些 body 返回 `422 invalid_metadata`,issue 落在 `submitBehavior.url`。所以本卡收口的是错误 路径而非静默入库:纠正现在出现在管理员能改的字段里,而不是一次失败的往返。 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 17, 2026
PM 验收 ✅ ACCEPT(#4990,批次 20) 实物核验(merge-base CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。 卡面开放点的实测回答(加分项):服务端确实拒绝该保存 —— 只读追到 反向验证读数:两向精确命中;变异 2(拒绝换本地文案)是承重探测器 —— 手写镜像会让拦保存半边继续绿、只红在文案出处,这正是断言对活 schema 解析比对而非抄字符串的理由。控制字节纪律两次自伤自纠(写控制字符测试时物化真实 tab),并点名 undraft + auto-merge(squash)。 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#4990
问题
apps/console/src/pages/developer/PublicFormsPage.tsx的saveEditredirect 分支只验一件事 —— 字段非空,然后client.meta.saveItem('view', …)。objectstack#7496(2026-08-11 裁定,objectstack#7657 落 spec,本仓
@objectstack/spec17.0.0 GA pin 已覆盖)把submitBehavior.url裁为仅相对路径,并对七族越契约值各给一句 author-facing 处方:空值、前导 scheme(https:,同样地javascript:/data:)、前导//、反斜杠、空白或 C0/DEL 控制字符、非良构 token 的花括号形状、不以/开头。这道门只执行了其中第一族。管理员可以输入
https://example.com/thanks或javascript:alert(1),得不到任何提示 —— 而这个界面自己刚教过他这个值可以用:字段是type="url"(该 type 自己认的「合法」恰是本键拒绝的绝对 URL),占位符写着https://example.com/thanks。「declared = enforced」在这里是反的:授权门比它所授权的 schema 更宽松。修法:复用同一次解析,不写第二份规则
按卡面形状,判定复用 #4992(#4190 消费侧)落地的那条 spec-schema 解析路径:
apps/console/src/components/submitRedirect.ts把其中的 shape 判定抽出为新导出checkSubmitRedirectUrl(url),返回 schema 接受后的值或 spec 自己那句 refusal;resolveSubmitRedirect(消费侧)改为调用它再做插值替换 —— 行为不变,少一份代码。role="alert"+aria-invalid+aria-describedby),并且不保存;输入即清除,作者在他想提交的那一刻被纠正一次,而不是逐键被追问。规则没有在对话框里重写一份:镜像拷贝在 spec 改动前的每次取值比较里都是绿的,正是
scripts/check-spec-symbol-derivation.mjs要劝退的形状(objectstack#4115、objectui#4074/#4588/#4592 各付过一次)。裁定日后放宽,跟着 pin 走,这里不需要改。附带两处:本地的
Redirect URL is required去掉(空值本就是七族之一,一并走契约,作者读到的是「目的地长什么样」而不是「必填」);字段改回纯文本输入、占位符/thanks、并给出规则提示 —— 它不该再教错值。边界(照卡面办)
thank-you的title/message不加验证:spec 里两者就是自由串,没有可供门陈述的契约。已用一个测试把这条边界钉住,免得后来人顺手扩大。服务端到底拒不拒(卡面的开放点,已测)
拒。 只读判定,未起任何服务:
packages/rest的PUT /meta/:type/:name委托protocol.saveMetaItem,其 spec-conformance gate 走resolveOverlaySchema→getMetadataTypeSchema('view')(@objectstack/spec/kernel,与 console 打包的同一个 17.0.0 GA pin)。用PublicFormsPage.saveEdit实际构造的 body 直接跑这条 gate:所以卡面「either way」里成立的是前一支:元数据语料不会积累这些值,本卡收口的是错误路径而非静默入库。这不削弱修法,反而是它的论据 —— 纠正现在出现在管理员能改的字段里,而不是一次失败的往返;而且门与服务端从构造上就同判,因为是同一个 schema。
未测的部分如实标注:这是在 gate 函数层面测的(服务端路由实际调用的那个函数与注册表),不是活的 HTTP 往返;某个部署的 spec 版本若与 GA pin 不同,结论随之不同。另有一处顺带观察但未能证实,故不据此立 issue:上面的 probe 必须补
viewKind才能让 body 落到formOverlay分支,缺它时同一条 gate 会对本页任何保存都 422(与 url 无关)—— 但本页保存的是getItems读回的it.spec,其真实形状要有真实存量数据才能判定,我没有。验证
pnpm exec vitest run apps/console/→ 54 files / 647 tests 全绿(含新增 15 + 扩充 20)。pnpm exec turbo run type-check --concurrency=2→ 81/81 successful。node scripts/check-control-bytes.mjs→ OK(4465 files);check-spec-symbol-derivation.mjs/ 两个 changeset 门 → OK。改动文件 eslint:0 error,warning 数与基线逐一相同(14 → 14,均为既有any/useEffect)。u0009那种写法),并在越门之外自扫[\x00-\x08\x0b\x0c\x0e-\x1f](stack渲染器读一个从未声明的spacing键,catalog 里 135 个节点照着它写 #4890 形态)。本 PR 正文自身也栽过一次:初版这一行里我把制表符写成了真实字节,读回存档时发现并改掉 —— 正是「写规则时把字节物化」的那个形状。反向验证(先书面预判,再变异实测)
预判写在两个测试文件的 docblock 里,commit 后变异、
git checkout --还原(未用 stash):submitRedirect.test.ts全绿(模块未动)Tests 11 failed | 58 passed,首个失败即Unable to find role="alert"saveItem那半仍绿;aria / 清除 / 合法值 / thank-you 全绿Tests 9 failed | 60 passed,失败点在:158 toHaveTextContent第二个变异是本卡真正要防的那件事的窄探测器:一份镜像拷贝会让
saveItem那半照旧通过,只在文案来源上失守 —— 所以断言比的是 schema 现场解析出来的那句话,而不是我抄进测试的字符串;spec 改词,测试照绿,拷贝规则,测试变红。