Repository navigation
fix(storage)!: a record write naming a file its caller may not read is refused at the field - #26237
Conversation
…s refused at the field A write that names a file its caller may not read, or an id the acting organization cannot see, now fails before the driver runs with one 400 VALIDATION_FAILED naming each such field with the ADR-0114 code invalid_value, the form door's envelope. The two answer identically and name the field only. Every named file is judged before any is copied, so a refused field never leaves a copy behind. System and context-less writes, and the outage envelope, are unchanged. Replaces the quiet answer (the id kept as written) in the ADR-0104 2026-10-26 addendum. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGkwyoUjm7LjHY7uBXFSgv
…rned link lifetime The download door now picks the signed-link lifetime from the same recognition the read verdict uses (releasingRecordOf), so every file a record's rule governs gets the record-governed lifetime. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGkwyoUjm7LjHY7uBXFSgv
…dia fields The matrix wrote ids that name no file, relying on the write keeping them as written. Such a write is now refused at the field, so the HTTP suite uploads one real file per media field and asserts the read form carries its id. The matrix keeps its stored-shape stand-ins for the value-shape test. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGkwyoUjm7LjHY7uBXFSgv
📓 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 554631561a71c70a99c8a70a1671ce0ac4db4257 && git checkout 554631561a71c70a99c8a70a1671ce0ac4db4257
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df8aac41f2d9d14db04afdfe48511216419dafc0 c075544b6b3d23fad8f4da0fc0c7ede8459a511d && git checkout -B drift-repro df8aac41f2d9d14db04afdfe48511216419dafc0 && git merge --no-ff c075544b6b3d23fad8f4da0fc0c7ede8459a511d
node scripts/docs-audit/affected-docs.mjs --json df8aac41f2d9d14db04afdfe48511216419dafc0
|
Contract reviewServed-tier: Inputs: card #26043 (body, triage 6414315649, claim 6418687865 and its amendment 6421454634, os-dev report 6421410477), PR #26237's body and file list, the net diff of the three commits against main at the merge base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读改了什么:记录写入时,如果文件字段里填的是调用者无权读取的文件,或本组织里不存在的文件 id,现在在该字段上直接拒绝,返回 为什么改:静默保留会让用户看到"保存成功但文件打不开",这正是 ADR-0104 的 风险与代价(含回滚):这是收窄,以前能保存的这类写入现在会被拒。系统写入和无上下文的程序写入不变,鉴权存储故障仍按原信封失败。已按旧规则存下这类 id 的记录,带原值再更新时会被拒,需要清空字段或换成可读文件。回滚即 revert 本 PR。 席位意见:建议批准。合约复核( 你要做的:在 PR 上批准(Approve)。批准后本席位清标签、转 ready、开自动合并。 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. This PR touches
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #26043
Clause-②: no (narrowing)
Metadata-protocol: none
What this changes
A record write that names a file its caller may not read used to save, keeping the id as written (#25951's "quiet answer"). The field then held a file it did not own, and every reader got an unusable value. That is the state ADR-0104's
verify-referencescounts as blocking. Now the write is refused at the field.file-reference-lifecycle.ts). Copy-on-claim judges every file a write names before it copies any, through the one lookup both halves of the claim path already share (findReferenceableFile). A file the caller may not read, or an id the acting organization cannot see, refuses its field. The write fails before the driver runs with one400 VALIDATION_FAILEDwhosefields[]has one ADR-0114invalid_valueentry per refused field. The entry names the field and its label only, never the file or the record that holds it. The two cases answer with the same body, so the write door cannot tell "may not read" from "does not exist in this organization".accept/maxSize) never answer ahead of the read check.storage-routes.ts). A file governed by a record's rule now gets the record-governed link lifetime at the download door. The door and the read verdict read the same recognition (releasingRecordOfinmetadata-store.ts). Who gets a link does not change.Envelope spelling. The order and claim wrote
400 VALIDATION_ERROR. They also say to match the #25682 form door and to carry aninvalid_valueentry. That door answers400 VALIDATION_FAILEDwithfields[], and in this codebaseVALIDATION_ERRORis the envelope with nofields[](ADR-0104's 2026-10-19 note on D2). So this PR usesVALIDATION_FAILED, the form door's envelope, and says so here.Declarations
Clause-②: no (narrowing), measured on this diff: a record write naming a file its caller may not read, or an id no file of the organization has, used to save and is now refused. Nothing that was refused becomes accepted.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).Tests
HEAD
c075544b6.3993793dc(the new pins, same files): 8 failed, 64 passed. That is the 7 refusal pins infile-reference-read-verdict.integration.test.tsand the 1 link-lifetime pin instorage-routes.test.ts. The controls (a system write still copies, a context-less write still keeps an unknown id) were green on base.file-reference-read-verdict.integration.test.ts(realObjectQL+ sqlite,StorageServicePluginbooted as shipped). It pins the whole refusal body withtoEqualfor a file owned by a record the writer cannot read, an attachments-scope file, a delegate refusal, an id that names no file, an update (the record keeps what it held), a multi-field write (nothing copied for the passing field), and a judgement fault. Each pin also checks that no record and nosys_filerow was written.file-reference-lifecycle.test.ts: the files: an authorization gap in the file-reference lifecycle, within one organization (detail withheld) #25692 unit pins move from "kept" to "refused". New pins: an unknown id gets the same body as an unreadable file, and every refused field is named in one answer.file-reference-release-read-rule.integration.test.ts(files: an authorization gap in the file lifecycle after a file is released, within one organization (detail withheld) #26046): a write by a member refused the record is nowrefusedinstead ofkept, under both collection settings.scripts/ablation-replace.mjs. Disabling the refusal throw turns 27 tests red across the three files (bloba9069ab6eba1to37aa77f91e7a). Reverting the lifetime line turns the link-lifetime pin red (blob95d899032bcato95bfe351ee89). Both were restored, and the blob equals HEAD withgit diff HEADempty.pnpm --filter @objectstack/service-storage test: 66 files, 1226 tests passed.typecheck(includingcheck:test-typecheck) passed. eslint--no-inline-configonpackages/services/service-storage/src: clean.pnpm --filter @objectstack/dogfood testunder the verify lock, VERDICT command-exit 0, Test Files 296 passed | 3 skipped (299), Tests 2523 passed | 18 skipped (2541). The first run, one commit earlier, was red in one file (field-zoo-roundtrip.dogfood.test.ts, the stale fixture above). After the fix that file andfield-zoo-value-shape.test.tspass 91/91, and the rerun at HEAD is the one quoted here.Gates
Local subset at HEAD
c075544b6:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 85 families from this diff. All 85 ran with exit 0, each code captured before any pipe, and--ranreconciles 85 run, 0 NOT MEASURED. One commit earlier,check-comment-mask-corpushit a 300 s local timeout on the loaded box andcheck-plugin-teardown-shape --self-testanswered exit 3 (shallow clone). Both were 0 on rerun, the latter after fetching its control commit, and both are 0 in the HEAD run.pnpm --filter @objectstack/service-storage testandtypecheckran one commit earlier, at66398e79. The last commit touches only two dogfood test files. Every other family is CI's, and this PR's check-runs are the record.Acceptance notes
verify-referencesalready reports.维护者速读(草稿)
改了什么:记录写入时,如果文件字段里填的是调用者无权读取的文件(或本组织里根本不存在的文件 id),现在直接在该字段上拒绝:返回
400 VALIDATION_FAILED,只点名字段,不暴露文件或其所属记录。以前(#25951)是"静默保留 id、保存成功"。另一个提交让受记录规则约束的文件,下载签名链接用记录级的短有效期。ADR-0104 2026-10-26 补记里"quiet answer"一条改写为"field refusal"。为什么改:静默保留会让用户看到"保存成功但文件打不开",而这正是 ADR-0104 第 5 步
verify-references判为阻断的状态。同类平台的做法也是直接拒绝挂接。这是你「先改措辞,再批,另开两张卡」里的第一张卡。风险与代价(含回滚):这是收窄:以前能保存的这类写入现在被拒。系统写入和无上下文的程序写入不变,鉴权存储故障时仍按原信封失败。已经按旧规则存下悬空 id 的记录,再次带着该值更新时会被拒,需清空或换成可读文件。回滚即 revert 本 PR。
席位意见:
你要做的:审阅 ADR 那一条与修复,批准后合并(Tier H,保持 draft 等你)。
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01EGkwyoUjm7LjHY7uBXFSgv