Repository navigation
fix(storage)!: an anonymous write carries no file, at every anonymous door - #26036
Conversation
Red on main: the lifecycle claims, copies or keeps a value an anonymous caller wrote into a file field. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG2Ug3VQqDPcepHcmoD9m1
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG2Ug3VQqDPcepHcmoD9m1
Ruling C held at every anonymous write door, not only the public form door: a write whose caller has no user may not put a value in a file field. It is refused before the driver runs with the form door's envelope (400 VALIDATION_FAILED, invalid_value per field). Empty and absent file fields, signed-in callers, system writes and context-less programmatic writes are unchanged. Three existing fixtures gave a signed-in subject a user-less caller for convenience; they now name a user, their subjects unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG2Ug3VQqDPcepHcmoD9m1
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG2Ug3VQqDPcepHcmoD9m1
📓 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:
What this run could not see
Coarse fallback — 10 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 e435c5f6858bc52277571b5cdebf3b4f6639b161 && git checkout e435c5f6858bc52277571b5cdebf3b4f6639b161
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 431f065177de0cfc9d682b3b18692132390e0c57 8102869e46411275625bde02165a6f23adb74ed4 && git checkout -B drift-repro 431f065177de0cfc9d682b3b18692132390e0c57 && git merge --no-ff 8102869e46411275625bde02165a6f23adb74ed4
node scripts/docs-audit/affected-docs.mjs --json 431f065177de0cfc9d682b3b18692132390e0c57
|
Contract reviewServed-tier: ① Derived judgmentsAccept-set change (narrowing only). The one behavioural edit is a module-private predicate run first in both before-write hooks the storage lifecycle already registers (
Non-system flow data nodes cannot present a user-less context either: Placement is correct and complete for D2's write classes. Every engine write path dispatches the before hooks: single insert, Envelope (ADR-0112 / ADR-0114). The refusal is Public surface. Tests. 11 unit cases (6 refusal: four value shapes on insert, update, several fields in one envelope; 5 controls) and 7 integration cases (5 refusal across the file arms the #25692 read verdict distinguishes, incl. an unowned ② Semver level
Disclosure: the changeset and PR body describe the rule and the door class in ADR-0138 D2's own public vocabulary (a caller with no user may no longer put a value in a file field; which class the change closes). Neither carries a request shape, an object, a step sequence or a pre-fix outcome, so the disclosure discipline holds on both carriers. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Each refusal pin now also reads the error through the shared HTTP boundary mapping and asserts 400 with VALIDATION_FAILED (ADR-0112). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG2Ug3VQqDPcepHcmoD9m1
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
|
Correction to this PR body's dogfood line (the full-battery bullet, measured on Generated by Claude Code |
Requested by Leehom · project thread
Fixes #25694
Clause-②: no (narrowing)
Metadata-protocol: none
What this changes
Ruling C on #25572 ("an anonymous submission carries no file", landed at the public form door by #25682) now holds at every anonymous write door of ADR-0138 D2, as the maintainer chose for this card (「立卡,先实测再修」).
@objectstack/service-storage, the file-reference lifecycle). A record write whose caller has no user may not put a value in a file field (FILE_REFERENCE_TYPES). It is refused in the before-write hook, before the driver runs and before any file is looked up, so a refused write leaves nothing behind.400 VALIDATION_FAILED, onefields[]entry per refused field with the ADR-0114 field codeinvalid_valueand the field's label, built with the samevalidationFailureand judged with the same emptiness rule (isEmptyForRequired). No error code is added.mainbefore this change, and the rule above holds for it as well. The measurements are with the server seat.Why the lifecycle and not each door: one guard closes the whole class, including a door added later, and it sits on the path the claim file surface names. The class-4 door itself (
@objectstack/runtime) is not edited.Declarations
Clause-②: no (narrowing), measured on this diff: an anonymous write that carried a file value is refused. Nothing refused becomes accepted. The claim wroteClause-②: noand named this arm as expected.Metadata-protocol: none: nothing a metadata author writes changes.patchchangeset on@objectstack/service-storage,fix(storage)!:, with the BREAKING section and the ADR-0087 dispositionnot-required (no-migration-prescription).Three existing fixtures changed, with their subjects kept
sys-file-copy-backfill-organization-stamping.test.ts(three cases) andbackfill-sys-file-public-scope.test.tsdrove a copy-on-claim write with a caller that carried an organization and no user. Their subject is organization stamping and scope on the copy, not the caller, and a user-less caller is now the anonymous case. Each now names a user. Nothing they assert changed.Tests
All on head
b2f90ab2b5.pnpm --filter @objectstack/service-storage test: Test Files 65 passed (65), Tests 1197 passed (1197).typecheck(includingcheck:test-typecheck): exit 0.cb5bb350,be589179) and failed on the unchanged lifecycle; the fix is1171d6c2. The ablation below is the same proof on the final tree.file-reference-lifecycle.test.ts, new describe, 11 cases: refusal on insert for four value shapes, refusal on update, several fields named in one envelope by label or name, and five controls (empty or absent values, an anonymous update that leaves out or clears a file field, a signed-in caller, a system and a context-less write, an object with no file field). Each refusal assertscodeand thefields[]entry, and that nothing was written, no file was looked up and no bytes moved.file-reference-read-verdict.integration.test.ts, new describe, 7 cases on a realObjectQLoverSqlDriverwithStorageServicePluginbooted: insert and update refused for the guest envelope across the file arms the read verdict distinguishes, nothing written and no ownership moved, plus a guest write with no file and a signed-in control.eslint --no-inline-configwith the root config overpackages/services/service-storage/src: exit 0. The repo-widepnpm lintis CI's run.(narrowing)arm),pnpm --filter @objectstack/dogfood testunder the verify lock, no shard, no path, onb2f90ab2b5: Test Files 295 passed, 1 failed, 3 skipped (299); Tests 2522 passed, 1 failed, 18 skipped (2541). The one failure isauthz-probe-blind-spot.test.ts, thepackages/runtime/src/domains/i18n.tscontrol count (expects 3handleI18nRequest, finds 4). It is red onmainindependently of this diff: neither that test nor that file differs between this branch's base05f7758fand currentmain39a78b15, and this diff touches neither package.Proof
Through
node scripts/ablation-replace.mjs(anchor 1 to 0, blob changed, restored blob equals HEAD,git diff HEADempty). With the anonymous-caller test replaced by an always-pass, 12 pins redden: the 6 unit refusal pins, the 5 integration refusal pins, and the integration signed-in control, which follows from the guest pins having left the file claimed. The 5 unit controls and the guest no-file control stay green. Both suites import the lifecycle through relativesrcpaths, so nodist/is involved.Gates
b2f90ab2b5bynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; the change set is from the merge base): 71 commands, each exit code captured before any pipe. 70 exited 0 in sequence.pnpm check:dual-build-cjs-loadsfirst exited 3 (seven packages outside the dogfood closure had nodist/, nothing measured), and after building those seven its rerun exited 0.--ran: 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.Acceptance notes
servemounts). A kernel composed without the storage service has no platform files for an anonymous write to carry.Generated by Claude Code