Repository navigation
feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) - #22469
Conversation
…e it at the upload doors Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
…ge scope public Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
…ope public Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
…tire-public-storage-scope
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 141 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 053c265ec3c4268fe75ff93ead4dc37ccc371244 && git checkout 053c265ec3c4268fe75ff93ead4dc37ccc371244
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4e9fe9ff6afbc1957039b351f8d4ca88948865cf cb35f90d4da4c0b96a2b07bbfcd29461d726d98b && git checkout -B drift-repro 4e9fe9ff6afbc1957039b351f8d4ca88948865cf && git merge --no-ff cb35f90d4da4c0b96a2b07bbfcd29461d726d98b
node scripts/docs-audit/affected-docs.mjs --json 4e9fe9ff6afbc1957039b351f8d4ca88948865cf
|
Contract reviewServed-tier: Reviewing seat Check-runs on the head: 35 runs, every one ① Derived judgmentsEvery accept-set and public-surface change the net diff implies, each judged:
Nothing in the diff is judged wrong. One observation, not a finding: the prescription's version string is a prediction; it goes stale only if a release is cut before this lands, which every retirement prescription here accepts. ② Semver levelChangeset
③ Boundary flagsDev report
Implemented-by: VERDICT: PASS Landing note for the owning seat: no governed path, so no tier lifts; the card stays open on its decision; |
…tire-public-storage-scope
…the protocol-18 merge Pure regeneration (gen:spec-changes, gen:upgrade-guide) on top of the merge of origin/main 4e9fe9f, which projects step 18 into both documents. The only delta is this branch's D3 semantic entry storage-scope-public-retired: step 17 to 18 migrated 329 to 330, aggregate 16 to 18 migrated 406 to 407, the guide's step-18 semantic list 329 to 330. No hand edit. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
Contract review carried over a pure regeneration —
|
Part of #22443
Clause-②: no
Retires the storage scope
public, which never made a file publicly readable, and keepsacl: 'public_read'(ADR-0104) the one opt-in for anonymous download: the triage answer on the card ("trim"). This PR lands the half that every remaining option shares. The other half, thesys_fileobject'spublicscope option and rows already stored with it, is a decision rather than an edit, and stays on the card (see "Left for a decision"). #22443 remains open.What lands
StorageScopeSchema(@objectstack/spec,system/object-storage.zod.ts).publicleaves the enum throughenumWithRetiredValues.tscrefuses it, and a parse, alone or asObjectStorageConfig.scope, answers with a prescription that namesacl: 'public_read'instead of zod's generic enum message.registerStorageRoutes(presigned and chunked,@objectstack/service-storage). A request naming scopepublicis answered400 INVALID_REQUESTbefore any file row, session row, upload URL or backend multipart exists.INVALID_REQUESTis an existing code, registered under this package in the ADR-0112 ledger and already used by these handlers for a request they cannot take. The message names the remedy and where it lives: on the stored file record, because the upload request carries noacland every upload is storedacl: 'private'. Every other scope, and an omitted one, is taken exactly as before.api/storage.zod.ts). Thescopedescription onGetPresignedUrlRequestSchemaandInitiateChunkedUploadRequestSchemano longer offerspublicas an example, and says what the scope is and is not. The shape is unchanged (z.string()).storage-scope-public-retired(step 18), and one changeset:minoron both packages, a declared narrowing, dispositionregistered storage-scope-public-retired. No D2 conversion (H2 below).spec-changes.jsonand the upgrade guide do not move, because step 18 is not projected yet.The download handlers are not touched, so their verdict from PR #22439 stands. A pin here asserts it on an already-stored
public-scoped row with the default acl: anonymous401 AUTH_REQUIRED, signed-in200.Measurements: the four dispatch hypotheses
All readings at origin/main da159f7 unless stated.
StorageScopeSchema. Consumed only byObjectStorageConfigSchema.scope, andObjectStorageConfigSchemahas no runtime consumer: no parse of it outsidepackages/spec.GetPresignedUrlRequestSchema.scopeandInitiateChunkedUploadRequestSchema.scope. Openz.string(), and the first one's description taughtpublic. No server handler parses a request with them.sys_fileobject'sscopeselect (user,tenant,public,private,temp,attachments). This is the stored vocabulary and, before this PR, the only enforcement: the engine refuses an insert outside it (invalid_option).sys_upload_session.scope. A freeField.text, copied from the request; no vocabulary.scope: 'public'has one hit in the tree, a spec request-schema test fixture (re-pointed totenanthere), against 29 hits forscope: 'attachments', the control. At the.objectui-shapin f0268ad784 and at objectui main 2063f7a: zero hits; the console passes no scope and the record attachments panel passesattachments(the control). No metadata type carries either schema, and storedsys_filerows are data, which the ADR-0087 chain does not reach. Measured on a realObjectQLover sqlite, with the realSystemFileminus itspublicoption and a legacypublicrow written through the driver:findOneandfindpass the row through, and an update that does not namescopesucceeds. Reading is unaffected.copyOwnedFileinfile-reference-lifecycle.ts) re-inserts the source row's scope, and that insert is refusedVALIDATION_FAILED,invalid_optiononscope. An update that writesscope: 'public'is refused the same way.publicfile id into a second record field. Per the dispatch, that is a fork for the maintainer, not a choice for this PR.INVALID_REQUESTis reused; no new code, soClause-②staysno.storage-route-ledger.ts. Holds. Its'public'is a route disposition (an unauthenticated browser route), not a storage scope. Untouched.Left for a decision
The
sys_filescopeselect keeps itspublicoption in this PR. The options, as measured above:publicrows touser, once. A storage-service backfill, the shape thesys_fileorganization backfill already takes. Declared and enforced agree everywhere afterwards. It costs a data rewrite on every deployment, a no-op where there are no such rows. Keys keep theirpublic/prefix and no bytes move.The recommendation, reasoned on the four axes, is in the report on the card.
Tests
All at
149062a9db(this branch merged with origin/main 3ca71b6):@objectstack/service-storage:vitest run, 47 files and 789 tests passed.typecheckexit 0.@objectstack/spec:vitest run --project local, 629 files and 18783 tests passed (1 todo).typecheckexit 0, which includes the test layer, so the@ts-expect-erroron the retired member compiles as a real check. Repo-project files that read these surfaces (retired-key-migrate-sentence,step18-rationale-merge,conversions-major18-merge,error-catalog-docs): 4 files and 41 tests passed. The rest of--project repois declared to CI.dispatch-gates --commandsderived 119 families on this head, pluscheck:generatedfrom the dispatch list. All 119 ran with exit 0, anddispatch-gates --ranreports 119 run, 0 not measured, 0 unrun..tsfiles, 8 files in the JSON output, 0 errors, 0 warnings. The population iseslint.config.mjs's own TS glob, andeslint.config.mjsnever enables type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis CI's.scripts/ablation-replace.mjsand restored togit diff HEADempty:publicre-admitted to the enum: both prescription pins go red (2 failed, 64 passed), and the unknown-value control stays green. A first run of this ablation used the dummy retired keypublik, which is the control's own value, so it reddened the control as well. It was rerun with an unrelated key; the reading above is the rerun.Acceptance notes
sys_fileselect, a free key prefix for instance, is answered500 INTERNALby the upload-starting handlers. The engine'sinvalid_optionis relayed as an internal error whose message tells an operator to restore the data engine. Measured with the presigned handler over a real engine. objectui's upload adapter documentsscopeas a free logical key prefix, with examples that are not select options. Not fixed here: a different defect, with its own shape to decide.ObjectStorageConfigSchema, and with it the rest ofStorageScopeSchema, has no runtime consumer. Noted, not filed (an unconsumed declaration with no pull).acl: 'public_read'has no setter at either upload handler (both storeprivate), and ADR-0104's "the field declares a public posture" half has no spec key. So the remedy this PR names can be applied only by a write to the storedsys_filerow. Noted for the guest-model family (design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146).StorageScope.Generated by Claude Code