A schema's {param} names are never checked against the page that has to fill them #36

Open
opened 2026-08-15 21:05:14 +02:00 by thisilike · 0 comments
Owner

Split out of #31 (review comment on c62d5b6), where the confirm-phrase case was fixed on its own. This is the general version.

What is missing

A schema page declares its params in its own path — stack/{project} — and then interpolates {param} into a dozen other strings: the page title, every block source, parent.page, an action's path / page / open / when.source, a form's submit.path and successPage, a live topic. packages/shell/src/schema/resolve.ts substitutes "" for a name the page does not have, and that is the whole error handling.

packages/server/src/modules/ui-schema.ts validates plenty — that a page path is relative, that paths are well-formed and their percent escapes are, that page paths are unique, that a root page exists, that an action has exactly one of path/page/open — but never that a {name} appearing in one of those strings is a name the page can supply. A module can ship source: "/stack/{proejct}" and nothing anywhere says so.

Why it matters unevenly

The failure mode depends on where the typo lands, and only some of them are loud:

Where What happens
block source, action path, form submit.path request to a URL with an empty segment — usually a 404 somebody sees
parent.page, action page, successPage navigation to a route that does not resolve
page title renders with a hole in it
live topic subscribes to a topic nobody publishes — no error at any layer, the page simply never updates and looks like a slow backend
confirmPhrase was the worst: the phrase came out empty, an empty phrase means "no phrase required", and the typed-confirmation gate silently disappeared. Fixed in #31 by falling back to the literal template

The confirmPhrase one is fixed because a vanishing safety gate could not wait. The rest are still one typo from a bug whose cause is invisible at the point where it hurts.

Where the check belongs

In ui-schema.ts, as a page-level refinement — not in the web renderer. ctx.ui.register is validated once, server-side, and every client renders what survives that: the web schema renderer, the Android app, anything later. A check in the renderer fixes one surface and leaves the others believing the schema.

Shape of it: collect {name} from the page's own path, then walk every interpolated string on that page and require each placeholder to be in that set.

Two things it must not flag:

  • list.link = { page, params } — those values are data paths resolved against the row, not placeholders (ui-schema.ts:94), so the target page's params come from the row and not from the current page.
  • Anything a module deliberately spells with braces that is not a placeholder, if such a thing exists — worth a grep across the built-in schemas before deciding the regex is the whole rule.

Open question worth deciding rather than defaulting: refuse or warn? A validation error disables the module, which is the existing contract for a malformed schema and catches this before it ships. That is my preference, but it turns a page-title typo into a dead module, so it is a real call. A middle option is refusing only where the failure is silent (topic, confirmPhrase) and logging the rest — at the cost of a rule nobody can remember.

Also worth having

A test per failure mode, since the point of the check is that these are currently invisible. packages/server/tests/ui_schema_test.ts is the home.

Split out of #31 (review comment on `c62d5b6`), where the confirm-phrase case was fixed on its own. This is the general version. ## What is missing A schema page declares its params in its own path — `stack/{project}` — and then interpolates `{param}` into a dozen other strings: the page `title`, every block `source`, `parent.page`, an action's `path` / `page` / `open` / `when.source`, a form's `submit.path` and `successPage`, a live `topic`. `packages/shell/src/schema/resolve.ts` substitutes `""` for a name the page does not have, and that is the whole error handling. `packages/server/src/modules/ui-schema.ts` validates plenty — that a page path is relative, that paths are well-formed and their percent escapes are, that page paths are unique, that a root page exists, that an action has exactly one of `path`/`page`/`open` — but never that a `{name}` appearing in one of those strings is a name the page can supply. A module can ship `source: "/stack/{proejct}"` and nothing anywhere says so. ## Why it matters unevenly The failure mode depends on where the typo lands, and only some of them are loud: | Where | What happens | |---|---| | block `source`, action `path`, form `submit.path` | request to a URL with an empty segment — usually a 404 somebody sees | | `parent.page`, action `page`, `successPage` | navigation to a route that does not resolve | | page `title` | renders with a hole in it | | live `topic` | subscribes to a topic nobody publishes — **no error at any layer**, the page simply never updates and looks like a slow backend | | `confirmPhrase` | was the worst: the phrase came out empty, an empty phrase means "no phrase required", and the typed-confirmation gate silently disappeared. Fixed in #31 by falling back to the literal template | The `confirmPhrase` one is fixed because a vanishing safety gate could not wait. The rest are still one typo from a bug whose cause is invisible at the point where it hurts. ## Where the check belongs In `ui-schema.ts`, as a page-level refinement — not in the web renderer. `ctx.ui.register` is validated once, server-side, and every client renders what survives that: the web schema renderer, the Android app, anything later. A check in the renderer fixes one surface and leaves the others believing the schema. Shape of it: collect `{name}` from the page's own path, then walk every interpolated string on that page and require each placeholder to be in that set. Two things it must not flag: - `list.link = { page, params }` — those values are **data paths** resolved against the row, not placeholders (`ui-schema.ts:94`), so the target page's params come from the row and not from the current page. - Anything a module deliberately spells with braces that is not a placeholder, if such a thing exists — worth a grep across the built-in schemas before deciding the regex is the whole rule. Open question worth deciding rather than defaulting: **refuse or warn?** A validation error disables the module, which is the existing contract for a malformed schema and catches this before it ships. That is my preference, but it turns a page-title typo into a dead module, so it is a real call. A middle option is refusing only where the failure is silent (`topic`, `confirmPhrase`) and logging the rest — at the cost of a rule nobody can remember. ## Also worth having A test per failure mode, since the point of the check is that these are currently invisible. `packages/server/tests/ui_schema_test.ts` is the home.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OpsDeck/core#36
No description provided.