Repository navigation
Commit 6ad226a
feat(lint)!: refuse a form open to anonymous intake that declares a file field, at validate, compile and the publish gate (#25737)
Fixes #25650
Clause-②: no (narrowing: a form view open to anonymous intake whose
sections declare a field of a `FILE_REFERENCE_TYPES` type is refused at
`objectstack validate`, `compile` and the publish gate)
Metadata-protocol: narrowed
Ruling-ref: ruling C on #25572 (the maintainer, 「25619 C 25495 B」,
recorded on PR #25619 with the seat record `6383110676`); triage
`6385114404`
The protocol half of ruling C:
「访客表单不收附件。供应商先注册账号、登录后再填表上传…发布时响亮拒绝「公开表单声明了文件字段」」. The server half (the
submit door refusing a file value) is #25572 / PR #25682 and is not
touched here.
## What changes
- **New gating authoring rule** `validatePublicFormFileFields`
(`packages/lint/src/validate-public-form-file-fields.ts`, rule id
`public-form-file-field`, exported as `PUBLIC_FORM_FILE_FIELD`). For
every form a view opens to anonymous intake, it reads the fields its
sections declare and refuses any whose type on the bound object is in
`FILE_REFERENCE_TYPES` (`image`, `file`, `avatar`, `video`, `audio`). It
names the field, the object and the entry path. The prescription is the
ruled route: the visitor registers and signs in, then fills a form that
is not public.
- **Which forms are open** comes from the one rule both anonymous doors
serve from: `anonymousFormIntakeCandidates` (`@objectstack/spec/ui`).
That means `enabled: true`, `allowAnonymous: true` and a `publicLink`.
It covers all three form shapes: a view's default `form`, each
`formViews` entry, and the flattened `config` of a `viewKind: 'form'`
item. Both section entry spellings are read (bare name and `{ field }`),
as both doors read them, in `sections` and in the legacy `groups` bucket
(`os lint` hands the rule the unparsed stack).
- **The door that sees both halves.** A field's type lives on the object
and `sharing` lives on the view, so a view-level `superRefine` cannot
judge this. The rule is a stack-level entry in `AUTHORING_RULES`
(`packages/lint/src/authoring-rules.ts`), the registry `os validate`,
`os build` and `os lint` run. It also runs at the runtime publish gate
with `runtimeTypes: ['view']`: a view write's snapshot carries `objects`
as resolution context (`buildRuntimeWriteSnapshotSet`,
`runtime-gate.ts`). So an active `view` save, or a draft promotion,
answers `422 INVALID_METADATA` naming the entry. No door code moves.
- Docs: `content/docs/ui/forms.mdx` (a new "File fields" subsection with
the register-and-sign-in route),
`content/docs/ui/public-data-collection.mdx` (the whitelist cannot hold
a file field), `content/docs/deployment/validating-metadata.mdx` (the
rule's row in the door table), and the four CLI transcripts whose
printed author-time rule count moves from 52 to 53
(`check:docs-transcript-drift`).
- Changeset `.changeset/25650-anonymous-form-file-refusal.md`:
`@objectstack/lint` minor, BREAKING, with its ADR-0087 disposition
(not-required, no-migration-prescription).
## Lead re-measurements (dispatch's mechanism assumptions)
- **Open-to-anonymous is three switches, not one.**
`anonymousFormIntakeSlug` requires `enabled === true`, `allowAnonymous
=== true` and a non-empty `publicLink`. A form with `allowAnonymous:
true` alone is served by no door and is not refused. A pin covers each
switch.
- **`anonymousFormIntakeUnavailability` lives in
`@objectstack/metadata-core`, not in the spec module.** The spec module
holds the candidates half. Not read here: posture is a deployment
question, and the publish-time refusal is a metadata question.
- **"A field directly in the form" does not exist.** `FormViewSchema` is
strict and has no `fields` key. Both anonymous doors read
`form.sections[].fields[]` only (`form-routes.ts`: the GET schema
narrowing and the submit `allowedFields`). The pins cover a direct entry
in a section, a later section, the legacy `groups` bucket, and both
entry spellings.
- **Producers: 0.** None is broken. `examples/app-crm` was measured
through the built `@objectstack/lint`: 0 findings, and 1 for a file
field planted on its public form. `app-todo` and `app-multi-package`
have no public forms. `app-showcase`'s one public form, read in source
(`inquiry.view.ts`), uses `name`, `email`, `company`, `message`: text
and email. `platform-objects` has no public forms. HotCRM at `a4b3560`
has two public forms (`src/sales/views/lead.view.ts:941`,
`src/service/views/case.view.ts:527`) whose fields are text, email, url,
select, currency, number and markdown. objectui at `a3c0da9` authors no
public form; its hits are renderer code and tests.
## Verification (head `f6099df44`)
- `pnpm --filter @objectstack/lint test`: 162 files / 7293 tests passed.
This includes the new `validate-public-form-file-fields.test.ts` (13)
and the view-rule roster pins updated in
`authoring-rule-wiring.test.ts`, `runtime-gate.test.ts` and
`runtime-gate.page-visibility-predicate-writes.test.ts`.
- `pnpm --filter @objectstack/metadata-protocol test`: 263 files / 21489
tests passed. This includes 4 new door pins in
`protocol.runtime-authoring-gate.test.ts`: the active save is refused
with `422 INVALID_METADATA` and nothing is stored; the draft to active
promotion is refused; the door and `os build` give the same message and
hint; the form without the file field saves. This run was on
`e0aa3cb98`, before the docs-only and merge commits.
- `pnpm --filter @objectstack/cli test` (unit + integration): 500 files
/ 6291 tests passed (on `d4c18b25`; later commits are docs and a `main`
merge touching only `skills/` and release CI).
- Consumer files that run the registry: objectql (3 files, 29),
platform-objects (1, 2), plugin-approvals (2, 24), service-mailbox (1,
4), all passed.
- `pnpm --filter @objectstack/lint --filter
@objectstack/metadata-protocol run typecheck`: exit 0.
- Full dogfood (the title carries `!`): `pnpm --filter
@objectstack/dogfood test` passed, 295 files / 2520 tests (3 files and
18 tests skipped). Lock verdict `command-exit 0`.
- Derived gates: `dispatch-gates.mjs --commands` gave 97 commands. All
97 exit 0, with `--ran` reconciling 97 derived / 97 run / 0 unrun. Two
of them, `check:dts-closure` and `check:dual-build-cjs-loads`, first
went red on `plugin-auth`'s `dist/` while the dogfood closure build was
rewriting it concurrently, and were re-run green. The first-round red on
`check:docs-transcript-drift` was real (rule count 52 to 53) and is
fixed in this PR.
- **Ablation** (`scripts/ablation-replace.mjs`, on the committed fix):
the refusal guard was made always-skip (`|| fieldName !==
'ablation_25650_never'`), lint rebuilt, and the marker proved in `dist/`
(4 files). Prediction: 8 of 13 lint pins red and 3 of 4 door pins red.
Observed: 8 failed / 5 passed and 3 failed / 1 passed, exactly the
predicted tests. Restored blob-identical (`3325e760befb`, `git diff
HEAD` empty). On the restore leg, lint was rebuilt, the marker proved
absent (`--absent`), and both suites ran green again (13/13, 4/4).
## Acceptance notes
- An `object` write is not judged by this rule: its runtime snapshot
carries no views. Changing a field's type to a file type under a
published public form is caught by `objectstack validate` / `compile`
and by that view's next publish, not by the object's save.
`runtimeTypes` is `['view']` only, because declaring `object` would run
the rule over nothing.
- The bound object is resolved in the doors' rung order
(`anonymousFormObjectName` in `@objectstack/metadata-core`: the form's
own binding, then the view's list block, then its default form, then the
view). Lint cannot import `metadata-core`, so the rule spells that order
over lint's shared `viewObjectName` reader. Moving
`anonymousFormObjectName` into `@objectstack/spec/ui`, beside the
candidates half, would leave one copy. It is outside this card's
declared file surface, so it is noted here and not done.
- A section that names a field `group` instead of listing fields is not
read for file fields. Neither anonymous door serves a group's members,
so such a field is never on the public form.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f9a1e93 commit 6ad226a
15 files changed
Lines changed: 517 additions & 4 deletions
File tree
- .changeset
- content/docs
- deployment
- getting-started
- ui
- packages
- lint/src
- metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
758 | 758 | | |
759 | 759 | | |
760 | 760 | | |
761 | | - | |
| 761 | + | |
762 | 762 | | |
763 | 763 | | |
764 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
| 485 | + | |
485 | 486 | | |
486 | 487 | | |
487 | 488 | | |
| |||
741 | 742 | | |
742 | 743 | | |
743 | 744 | | |
744 | | - | |
| 745 | + | |
745 | 746 | | |
746 | 747 | | |
747 | 748 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
308 | | - | |
| 308 | + | |
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
243 | 256 | | |
244 | 257 | | |
245 | 258 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
393 | | - | |
| 393 | + | |
394 | 394 | | |
395 | 395 | | |
396 | 396 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
| 514 | + | |
| 515 | + | |
514 | 516 | | |
515 | 517 | | |
516 | 518 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| 131 | + | |
131 | 132 | | |
132 | 133 | | |
133 | 134 | | |
| |||
1569 | 1570 | | |
1570 | 1571 | | |
1571 | 1572 | | |
| 1573 | + | |
| 1574 | + | |
| 1575 | + | |
| 1576 | + | |
| 1577 | + | |
| 1578 | + | |
| 1579 | + | |
| 1580 | + | |
| 1581 | + | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
1572 | 1593 | | |
1573 | 1594 | | |
1574 | 1595 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
263 | 267 | | |
264 | 268 | | |
265 | 269 | | |
| |||
0 commit comments