Repository navigation
feat(lint)!: refuse a form open to anonymous intake that declares a file field, at validate, compile and the publish gate - #25737
Conversation
Claude-Session: https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR Co-authored-by: Claude <noreply@anthropic.com>
…at the view publish door Claude-Session: https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR Co-authored-by: Claude <noreply@anthropic.com>
…nd-sign-in route Claude-Session: https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR Co-authored-by: Claude <noreply@anthropic.com>
… table Claude-Session: https://claude.ai/code/session_01UDTkSkW5Hks1YsBoNomhDR Co-authored-by: Claude <noreply@anthropic.com>
…onymous-form-file-refusal
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d086fe5c1a5be7c40561c5a300b3260e904a9ab6 && git checkout d086fe5c1a5be7c40561c5a300b3260e904a9ab6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4cc17db3c063c741abeb503750007acf287ac0c9 e3165041aa01761f6e0ee70c43c7cb37f5540cfe && git checkout -B drift-repro 4cc17db3c063c741abeb503750007acf287ac0c9 && git merge --no-ff e3165041aa01761f6e0ee70c43c7cb37f5540cfe
node scripts/docs-audit/affected-docs.mjs --json 4cc17db3c063c741abeb503750007acf287ac0c9
|
…onymous-form-file-refusal
席位核对ACCEPT · #25650 ·
Generated by Claude Code |
Contract reviewServed-tier: Written 2026-10-25T04:50Z · face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flagsDev reports: 6392970741, superseded by 6393451226 on #25650 (head
Implemented-by: VERDICT: PASS The refusal is drawn on exactly the forms the anonymous doors serve and exactly the field types the spec names, runs at every door the ruling asked for with one message, is pinned in both directions and ablated, and widens nothing. Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
Fixes #25650
Clause-②: no (narrowing: a form view open to anonymous intake whose sections declare a field of a
FILE_REFERENCE_TYPEStype is refused atobjectstack validate,compileand 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); triage6385114404The 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
validatePublicFormFileFields(packages/lint/src/validate-public-form-file-fields.ts, rule idpublic-form-file-field, exported asPUBLIC_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 inFILE_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.anonymousFormIntakeCandidates(@objectstack/spec/ui). That meansenabled: true,allowAnonymous: trueand apublicLink. It covers all three form shapes: a view's defaultform, eachformViewsentry, and the flattenedconfigof aviewKind: 'form'item. Both section entry spellings are read (bare name and{ field }), as both doors read them, insectionsand in the legacygroupsbucket (os linthands the rule the unparsed stack).sharinglives on the view, so a view-levelsuperRefinecannot judge this. The rule is a stack-level entry inAUTHORING_RULES(packages/lint/src/authoring-rules.ts), the registryos validate,os buildandos lintrun. It also runs at the runtime publish gate withruntimeTypes: ['view']: a view write's snapshot carriesobjectsas resolution context (buildRuntimeWriteSnapshotSet,runtime-gate.ts). So an activeviewsave, or a draft promotion, answers422 INVALID_METADATAnaming the entry. No door code moves.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/25650-anonymous-form-file-refusal.md:@objectstack/lintminor, BREAKING, with its ADR-0087 disposition (not-required, no-migration-prescription).Lead re-measurements (dispatch's mechanism assumptions)
anonymousFormIntakeSlugrequiresenabled === true,allowAnonymous === trueand a non-emptypublicLink. A form withallowAnonymous: truealone is served by no door and is not refused. A pin covers each switch.anonymousFormIntakeUnavailabilitylives 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.FormViewSchemais strict and has nofieldskey. Both anonymous doors readform.sections[].fields[]only (form-routes.ts: the GET schema narrowing and the submitallowedFields). The pins cover a direct entry in a section, a later section, the legacygroupsbucket, and both entry spellings.examples/app-crmwas measured through the built@objectstack/lint: 0 findings, and 1 for a file field planted on its public form.app-todoandapp-multi-packagehave no public forms.app-showcase's one public form, read in source (inquiry.view.ts), usesname,email,company,message: text and email.platform-objectshas no public forms. HotCRM ata4b3560has 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 ata3c0da9authors 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 newvalidate-public-form-file-fields.test.ts(13) and the view-rule roster pins updated inauthoring-rule-wiring.test.ts,runtime-gate.test.tsandruntime-gate.page-visibility-predicate-writes.test.ts.pnpm --filter @objectstack/metadata-protocol test: 263 files / 21489 tests passed. This includes 4 new door pins inprotocol.runtime-authoring-gate.test.ts: the active save is refused with422 INVALID_METADATAand nothing is stored; the draft to active promotion is refused; the door andos buildgive the same message and hint; the form without the file field saves. This run was one0aa3cb98, before the docs-only and merge commits.pnpm --filter @objectstack/cli test(unit + integration): 500 files / 6291 tests passed (ond4c18b25; later commits are docs and amainmerge touching onlyskills/and release CI).pnpm --filter @objectstack/lint --filter @objectstack/metadata-protocol run typecheck: exit 0.!):pnpm --filter @objectstack/dogfood testpassed, 295 files / 2520 tests (3 files and 18 tests skipped). Lock verdictcommand-exit 0.dispatch-gates.mjs --commandsgave 97 commands. All 97 exit 0, with--ranreconciling 97 derived / 97 run / 0 unrun. Two of them,check:dts-closureandcheck:dual-build-cjs-loads, first went red onplugin-auth'sdist/while the dogfood closure build was rewriting it concurrently, and were re-run green. The first-round red oncheck:docs-transcript-driftwas real (rule count 52 to 53) and is fixed in this PR.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 indist/(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 HEADempty). On the restore leg, lint was rebuilt, the marker proved absent (--absent), and both suites ran green again (13/13, 4/4).Acceptance notes
objectwrite 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 byobjectstack validate/compileand by that view's next publish, not by the object's save.runtimeTypesis['view']only, because declaringobjectwould run the rule over nothing.anonymousFormObjectNamein@objectstack/metadata-core: the form's own binding, then the view's list block, then its default form, then the view). Lint cannot importmetadata-core, so the rule spells that order over lint's sharedviewObjectNamereader. MovinganonymousFormObjectNameinto@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.groupinstead 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