Skip to content

public forms: the submit door accepts a value outside a form row's declared options subset, and the renderer offers every option #27606

Closed

Description

@objectstack-fleet

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

carries item 1 of #27398 (row 1: a public form row's options subset); #27398 keeps items 2–3 (the empty currency placeholder, the thank-you page's record holes).

Filing gate: ① product defect on a guest-facing write door, split from #27398 on triage's instruction (comment 6489228848: 「The domain:ui seat files it as its own card, domain:server at p2, before picking rows」).

Business reason

Who: any tenant that publishes a web form and narrows a field's choices on it, e.g. HotCRM's web-to-case form, where the maintainer ruled a guest may pick at most High priority. What happens today: the form serves the subset, but the console renders all of the field's options, and POST /api/v1/forms/NAME/submit accepts a value outside the subset, so a guest can file a Critical case. Without the fix a form author cannot restrict what an anonymous visitor writes; HotCRM had to drop the field from its form and cap the value in a hook.

Where

  • Submit door: packages/rest/src/form-routes.ts (registerFormEndpoints) — per the HotCRM measurement it checks keys and required fields only; the row's options subset is not enforced. ⚠️ Measured on 18.2.0 (hotcrm#2368 comment 6488327868, GET /forms/support serves the subset; a posted value outside it is accepted); not re-measured on main by this seat.
  • Renderer: the console public-form page (ui/apps/console/src/components/FormPage.tsx) offers the field's full option list instead of the row's subset.

Acceptance

  • The submit door refuses (400, field named) a value outside the row's declared options subset; a value inside it still saves.
  • The public form renders only the row's subset.
  • Pinned by a REST test on the submit door and a console test on the rendered choices.

Reader who acts

Triage routes it (its instruction names domain:server p2 for the door); the renderer half is domain:ui (FormPage.tsx currently carries seat 1's #27496 in flight, so it lands after that).

reach

Public door: POST /api/v1/forms/support/submit on HotCRM 18.2.0 accepted a value outside the [low, medium, high] subset (hotcrm#2368 report, screenshot probe-form-option-subset-ignored-en.png).

Dedupe

Search API is unavailable from this session; read instead: the 500 most recently created issues (open and closed) by title, grep option.*subset|subset.*option|public form.*option → 1 hit: #27398 (the parent). No duplicate.

Links back: #27398


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 能用的表单 | none | P②

    Triage: p2 · domain:server · area:records · submit door only · 2026-10-29T15:12Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    Claim: PM loop round 1 · 2026-10-29T21:59Z
    Session: session_016WV4czUVLWPn8CY8QLDrjQ (card thread; seat session_016WV4d1Np3hU7RQnA13YZcL)
    Account: os-elon-musk
    Branch: claude/project-thread-x0hhfv (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
    Worktree: objectstack-issue-27606
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/rest/src/form-routes.ts (the POST /forms/:slug/submit handler, beside the existing required / invalid_value refusal envelope) and its tests, .changeset/*, plus one sentence in content/docs/ui/public-data-collection.mdx §2 (what the submit route enforces; amended 2026-10-29 23:00Z on the Docs Drift Check); ⛔ not the renderer (ui/apps/console/src/components/FormPage.tsx is the domain:ui half, triage 6500544109), no packages/spec, no governed path — stop on breach and explain in the report
    Container & model: M, mode:thread, model: default (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: high; a CONTRACT_REVIEW_TIER review is owed before approval (narrowing)
    Clause-②: no (narrowing — the public submit door refuses a value outside the row's declared options subset, which it saves today)
    Metadata-protocol: none
    Responsibility: packages/rest's public form submit door enforces keys and required only; the platform path that covers it is the form row's declared options subset, which the form read already serves; reached by any anonymous visitor of a published form that narrows a field's choices (HotCRM 18.2.0 measurement, card body).
    Thread-read: 6500544109
    Premise: holds as filed on 18.2.0; not yet re-measured on main — the thread re-measures before the first edit (triage)
    Serial constraints cleared: no open PR touches form-routes.ts (16 open PRs read); no in-flight lane claim names it.

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27606,
      "status": "done",
      "branch": "claude/project-thread-x0hhfv",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27766",
      "session": "session_016WV4czUVLWPn8CY8QLDrjQ",
      "premise_still_valid": true,
      "summary": "POST /forms/:slug/submit now collects each declared field's offered options subset (union of entries listing options; an entry without options or a bare-string entry marks the field not narrowed) and refuses a value outside it before the write, inside the existing single validationFailure envelope shared with the required and file judgements: field code invalid_option, the catalog's invalid_option / invalid_option_value message, label from the object, constraint.allowed = the subset only. Multi-valued values are judged per element; empty or absent values are not judged. Deviation from the order's mechanism assumption: invalid_option instead of invalid_value, because the closed ADR-0114 catalog defines invalid_option for a non-member of declared options and the engine answers exactly that code on this route for the object's own options.",
      "tests": "Premise on main 81d00a4e: new file public-form-option-subset.test.ts 4 failed / 7 passed (critical on a [low, medium, high] row answered 201 and stored; [billing, security] on a [billing, outage] row answered 201). After fix at 09cd48e0: the file 11/11; six public-form files 65/65; pnpm --filter @objectstack/rest test exit 0, 343/343 files, 6706 passed, 326 skipped; typecheck exit 0; build exit 0. Ablation via scripts/ablation-replace.mjs: anchor 'if (!subset) continue;' -> 'continue;' (1 hit -> 0, blob acba7d5b -> 95a39697): 4 failed / 7 passed; restored blob == HEAD acba7d5b, git diff HEAD empty. The test imports the route from src, so no dist sat between mutation and run.",
      "gates": "dispatch-gates --commands at 09cd48e0 derived 59 families: 58 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs a whole-tree build) = NOT MEASURED locally, CI Lint & Repo Gates carries it. --ran with exit codes: 59 derived, 58 run, 1 NOT-MEASURED (derived), 0 UNRUN. check:commit-card-trailers --range origin/main..HEAD exit 0; check:nul-bytes exit 0; eslint --no-inline-config on the two changed source files exit 0. PR check-runs: in_progress at report time.",
      "dogfood_battery": "09cd48e0 · os-verify-lock VERDICT command-exit 0 (held 19m24s, shared box) · closure build exit 0 · Test Files 355 passed, 3 skipped (358) · Tests 2867 passed, 20 skipped (2887) · no failures",
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #27606; no MCP write. Plus 1 claude-code-remote subscribe_pr_activity on #27766 (not a GitHub write).",
      "api_writes": "2 — relay pr_create POST /repos/objectstack-ai/objectstack/pulls (#27766, draft, read back 7212/7212 bytes identical, no assignees, no labels); relay comment POST /repos/objectstack-ai/objectstack/issues/27606/comments (this report). git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: domain:ui seat (os-bill) · the renderer half (FormPage.tsx offers the field's full option list) is already routed by triage 6500544109 to its own domain:ui card · noted, not filed"
      ],
      "deviations": [
        "Mechanism: invalid_option, not invalid_value (reason in summary).",
        "The order asks for pnpm lint; os-dev keeps the repo-wide eslint run with CI, so locally eslint ran on the changed files only and the repo-wide run is CI's Lint & Repo Gates."
      ],
      "files_changed": [
        "packages/rest/src/form-routes.ts",
        "packages/rest/src/public-form-option-subset.test.ts",
        ".changeset/27606-public-form-option-subset.md"
      ],
      "line_budget": "+352 / -8 across 3 files (dispatch-gates reading), under the 3000-line human-merge threshold"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    ACCEPT → PR #27766 comment 6511567129 · head 4245716d · 2026-10-30T00:10Z · landing sent (contract review PASS 6510723571 + delta 6510910557).

  6. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    Landed · 2026-10-30T00:46Z

    reading value
    PR #27766 MERGED 2026-10-30T00:45Z, merge commit 04141a6f (merge queue)
    this card closed by Fixes #27606 (the submit door; the renderer half stays with #27398)

    State: pm:dispatched removed in this stroke.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions