Repository navigation
fix(rest)!: an anonymous public-form submission carries no file - #25682
objectstack-fleet[bot] wants to merge 7 commits into
Conversation
…on scope Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
The submit door refuses a value in a file field, naming the field, with the existing VALIDATION_FAILED envelope. The file-reference claim path looks a referenced file up within the writing organization. Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
…anization-scope pins Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
…carries a value The intake-availability pin now asserts its own subject: one object read, the submit's field judgement, and none for the predicate. Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 9 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 24 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 2d82a9a1e996be9e75c2afe24c289adbcf1d4f3d && git checkout 2d82a9a1e996be9e75c2afe24c289adbcf1d4f3d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5eeb02c614f3aa297c17c2048b112f4b605543fd 80aab79ea2ace233d29d48c7c5d59c4c6088fa8d && git checkout -B drift-repro 5eeb02c614f3aa297c17c2048b112f4b605543fd && git merge --no-ff 80aab79ea2ace233d29d48c7c5d59c4c6088fa8d
node scripts/docs-audit/affected-docs.mjs --json 5eeb02c614f3aa297c17c2048b112f4b605543fd
|
席位核对ACCEPT (seat review,
Not landed in this act. The Generated by Claude Code |
Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
Fixes #25622
Fixes #25572
Clause-②: no (narrowing)
Metadata-protocol: none
What this changes
Ruling C on #25572 (the maintainer, verbatim: 「25619 C 25495 B」; the ruling record is on PR #25619): an anonymous public-form submission carries no file.
@objectstack/rest).POST /forms/:slug/submitrefuses a value in a field whose declared type is a file type (FILE_REFERENCE_TYPES:file,image,avatar,video,audio). It names the field and refuses before anything is written. The answer is the door's existing envelope,400 VALIDATION_FAILED, with onefields[]entry per file field: the ADR-0114 field codeinvalid_valueand the field's label. If the same submission also leaves arequiredfield empty, both are reported in one envelope. An empty or absent file field submits as before. Only the fields the form declares are judged, so the refusal names nothing thatGET /forms/:slugdoes not already publish. No error code is added.@objectstack/service-storage) now looks a referenced file up within the organization the write acts in. It uses the channel that already stamps a copied file for that organization, so the driver's tenant scope for the posture in force decides what the lookup sees. A referenced file id outside that scope is kept as written and is neither claimed nor copied, exactly as an id the platform does not manage. The module writes no organization comparison of its own. A write that carries no organization, and thesingleposture, look files up as before.Not in this PR: the publish-time refusal of a guest-facing form that declares a file field (#25650,
domain:spec), and any guest upload door (ruled out by C).Declarations
Clause-②: no (narrowing). The door refuses a request shape it accepted, and the claim path reaches fewer files. Nothing is widened. Both claims carryClause-②: nowith no arm; the dispatch asked for the arm to be judged, and AGENTS.md's Post-Task Checklist step 3 names(narrowing)for this case, so the arm is added.Metadata-protocol: none. Nothing a metadata author writes changes.patchchangeset on both packages,fix(rest)!:, with the BREAKING section, the migration line and the ADR-0087 dispositionnot-required (no-migration-prescription).check-adr-0087-registrationreads it as[BREAKING+bang+clause-②-narrowing]and passes.Composition with #25641
#25641 (
domain:spec, ADR-0138 D5) also editspackages/rest/src/form-routes.ts, inresolveFormBySlug, the submit door's context literal and the imports. This PR's edit sits in the submit door'srequiredjudgement, below both hunks, with unchanged lines between them. A localgit merge-tree --write-treeof this branch with #25641's head08b020c9d3exits 0 with no conflicted path (form-routes.tsis not a merge-driver path, so the local answer is GitHub's). Whichever PR lands second mergesmain.One existing pin changed, with its subject kept
public-form-intake-availability.test.ts, "single posture, walled object — accepted, and the object is not even read for the predicate", asserted that the whole submit read onlyviewmetadata. The submit now reads the object's declarations once whenever a declared field carries a value, as therequiredjudgement already did for forms with required entries. The pin now asserts['view', 'object']: exactly one object read, by the field judgement. A predicate that read the object on this posture would be a second read and would still turn it red.Tests
All on head
80aab79ea2, each package's exit code captured separately:pnpm --filter @objectstack/rest test: Test Files 321 passed (321), Tests 6417 passed, 326 skipped.pnpm --filter @objectstack/rest typecheck: exit 0. The test-layer program (tsconfig.test.json, counted with--listFiles) compiles both test files this PR touches.pnpm --filter @objectstack/service-storage test: Test Files 63 passed (63), Tests 1137 passed.typecheck: exit 0, and the new test file is in its test-layer program.packages/rest/src/public-form-file-field.test.tshas 11 tests: the refusal by value shape (a file id, an external URL, an inline file value), a multi-valued field, two fields at once, one envelope withrequired, four empty/absent controls and the undeclared-field boundary.packages/services/service-storage/src/file-reference-organization-scope.test.tshas 6 tests on a realObjectQLandSqlDriverunder theisolatedposture with the hooks installed as the plugin installs them: insert and update in one organization, two controls in the writing organization (claim, and a copy stamped for it), and thesingleposture. The refusal pins assertcodeandstatus(VALIDATION_FAILED,400) plus each entry's field, code and label. Message wording is not pinned; each message is only required to name its label.eslint --no-inline-configwith the root config overpackages/rest/srcandpackages/services/service-storage/src: exit 0.(narrowing)arm),pnpm --filter @objectstack/dogfood testunder the shared lock with no shard and no path: it was running when this PR opened, and its counts are in theos-dev-reportcomment on public forms: an authorization-adjacent gap in how a submission's file field is handled (detail withheld pending maintainer) #25622.Proof
Each leg mutated through
node scripts/ablation-replace.mjs, which verifies on disk that the anchor hit, the blob changed and the restore matched the HEAD blob. Both subjects resolve through relativesrcimports in their suites, so nodist/is involved.continue, the 6 refusal pins ofpublic-form-file-field.test.tsredden (the door answers201), and its 5 control and boundary pins hold. Restored: the blobe6855e87be17equals HEAD's andgit diff HEADis empty.single-posture pin held in every leg.Gates
80aab79ea2bynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; the change set comes from the merge base): 73 commands, each exit code captured before any pipe. 72 exited 0 in sequence.pnpm check:type-check-debtfirst exited 3 (its own closure build never got a turn on the shared verify lock, so nothing was measured), and its rerun under the lock exited 0 with no entry above its recorded number.--ran: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.check-adr-0087-registrationreads the changeset as[BREAKING+bang+clause-②-narrowing]with an accepted disposition, andcheck-changeset-no-majorreports no major bump.pnpm lint(the rooteslint . --no-inline-config) is CI's run. Locally it was run over the two touched packages only (above).80aab79ea2mergedorigin/mainat9dac9eb8d6. Since thenmainhas gained refactor moves inobjectql,driver-sqlandmetadata-protocoland the template pins, and one derivation input (scripts/sync-template-versions.mjs) changed upstream. The derived list did not change between the two merges. CI reads the PR's merge ref.Acceptance notes
GET /forms/:slugstill serves a declared file field in the form's sections, so a renderer may show an upload control that a visitor cannot use. That belongs to the publish half (spec(ui): a form view open to anonymous intake (sharing.allowAnonymous) that declares a file field is refused at publish — the protocol half of #25572 (ruling C) #25650), which refuses such a form at publish. It is not changed here.requiredmessage, which the door renders from the built-in catalog without a locale. No catalog key is added.Generated by Claude Code