Found while implementing objectui#4190. Filed unassigned and NOT fixed there: #4190's dispatched scope is the CONSUMER side (FormPage's redirect arm), and this is the producer side — a different half of the same ruling.
What
apps/console/src/pages/developer/PublicFormsPage.tsx, saveEdit (around :222-250), builds the submitBehavior that gets written to the view metadata. The redirect branch validates one thing — that the field is not empty:
case 'redirect':
if (!editBehaviorUrl) {
toast.error('Redirect URL is required');
return;
}
submitBehavior = { kind: 'redirect', url: editBehaviorUrl };
break;
and then client.meta.saveItem('view', editRow.name, next).
os#7496 (ruled 2026-08-11, landed by os#7657, live on this repo's @objectstack/spec 17.0.0 GA pin) rules this key relative-only, with interpolation restricted to {{record.field_name}} and every interpolated value URL-escaped when the redirect is built. The spec refines the value and refuses seven families with author-facing prescriptions: empty, a leading scheme (https:, and equally javascript: / data:), a leading //, any backslash, whitespace or C0/DEL controls, a brace shape that is not a well-formed token, and anything not starting with /.
This door enforces exactly the first of those seven. An admin can type https://example.com/thanks — or javascript:alert(1) — into this dialog and it is saved as view metadata, unexamined.
Why it matters even though the renderer now refuses
The consumer side is being hardened in objectui#4190 (the renderer refuses out-of-contract values loudly instead of following them), so the resulting navigation is safe. What remains is worse-shaped than a navigation bug:
- the metadata corpus accumulates values the contract rejects, discovered only at submit time by an end user seeing a refusal for something an admin was allowed to save;
- the admin gets no feedback at the moment they can actually fix it, and the console is the surface that taught them the value was acceptable;
- "declared = enforced" is inverted: the authoring door is more permissive than the schema it authors for. This is precisely the shape that lets AI-authored metadata errors hide and multiply — the door is where a wrong value should be refused.
Whether the server rejects the save is unmeasured (I did not exercise meta.saveItem against a live server). If it does, the defect is a poor error path rather than a silent save; if it does not, the value lands in the corpus. Either way this door should state the rule.
Shape of the fix, for whoever takes it
The contract's own schema is importable and is what apps/console/src/components/submitRedirect.ts (from #4190) already uses at the consumer: parsing a minimal form view carrying { kind: 'redirect', url } yields the spec's own author-facing refusal text, which is exactly what this dialog wants to display next to the field. So the fix is likely a shared reuse of that parse rather than a second copy of the rule — a hand-written mirror in the dialog is what scripts/check-spec-symbol-derivation.mjs exists to discourage.
Note the same dialog writes title / message for thank-you with no validation either; that is a much weaker case (both are free-form strings in the spec) and is mentioned only so the scope question is answered in advance.
Generated by Claude Code
Found while implementing objectui#4190. Filed unassigned and NOT fixed there: #4190's dispatched scope is the CONSUMER side (
FormPage's redirect arm), and this is the producer side — a different half of the same ruling.What
apps/console/src/pages/developer/PublicFormsPage.tsx,saveEdit(around:222-250), builds thesubmitBehaviorthat gets written to the view metadata. Theredirectbranch validates one thing — that the field is not empty:and then
client.meta.saveItem('view', editRow.name, next).os#7496 (ruled 2026-08-11, landed by os#7657, live on this repo's
@objectstack/spec17.0.0 GA pin) rules this key relative-only, with interpolation restricted to{{record.field_name}}and every interpolated value URL-escaped when the redirect is built. The spec refines the value and refuses seven families with author-facing prescriptions: empty, a leading scheme (https:, and equallyjavascript:/data:), a leading//, any backslash, whitespace or C0/DEL controls, a brace shape that is not a well-formed token, and anything not starting with/.This door enforces exactly the first of those seven. An admin can type
https://example.com/thanks— orjavascript:alert(1)— into this dialog and it is saved as view metadata, unexamined.Why it matters even though the renderer now refuses
The consumer side is being hardened in objectui#4190 (the renderer refuses out-of-contract values loudly instead of following them), so the resulting navigation is safe. What remains is worse-shaped than a navigation bug:
Whether the server rejects the save is unmeasured (I did not exercise
meta.saveItemagainst a live server). If it does, the defect is a poor error path rather than a silent save; if it does not, the value lands in the corpus. Either way this door should state the rule.Shape of the fix, for whoever takes it
The contract's own schema is importable and is what
apps/console/src/components/submitRedirect.ts(from #4190) already uses at the consumer: parsing a minimal form view carrying{ kind: 'redirect', url }yields the spec's own author-facing refusal text, which is exactly what this dialog wants to display next to the field. So the fix is likely a shared reuse of that parse rather than a second copy of the rule — a hand-written mirror in the dialog is whatscripts/check-spec-symbol-derivation.mjsexists to discourage.Note the same dialog writes
title/messageforthank-youwith no validation either; that is a much weaker case (both are free-form strings in the spec) and is mentioned only so the scope question is answered in advance.Generated by Claude Code