Repository navigation
fix(spec)!: the presigned upload request declares a whole, non-negative size and a non-empty filename and mimeType - #28136
Conversation
…ve size and a non-empty filename and mimeType GetPresignedUrlRequestSchema.size becomes int().min(0), and filename / mimeType take min(1), the accept set POST /api/v1/storage/upload/presigned enforces. The door's byte-count check now also refuses a fractional size, which minted a URL and a pending sys_file row no upload could complete. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvsQx1XxK9FrejoXno
…igned request narrowing Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvsQx1XxK9FrejoXno
📓 Docs Drift CheckThis PR changes 2 package(s): 2 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 — 170 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 2b06dfa2f04658ed7a0f07acaadbb531402d422e && git checkout 2b06dfa2f04658ed7a0f07acaadbb531402d422e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d6c0a75f1a4ebdc6c4cf81865226bf72db9f29f6 4293804621b82769ea8a42241aeb0c5b833c20ca && git checkout -B drift-repro d6c0a75f1a4ebdc6c4cf81865226bf72db9f29f6 && git merge --no-ff 4293804621b82769ea8a42241aeb0c5b833c20ca
node scripts/docs-audit/affected-docs.mjs --json d6c0a75f1a4ebdc6c4cf81865226bf72db9f29f6
|
… none and grades @objectstack/spec patch Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvsQx1XxK9FrejoXno
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm Contract-review-claim: In-seat at |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm Contract reviewServed-tier: Inputs read: card #27598 (body and all 5 comments, the seat's claim correction 6524182832 included), PR #28136 body, file list and the three-dot diff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix: none. Should-fix: S1. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 维护者速读
席位核对 · ACCEPT ·
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
Requested by Tesla · project thread
Fixes #27598
Clause-②: no (narrowing)
Metadata-protocol: none
维护者速读
GetPresignedUrlRequestSchema)现在声明size必须是非负整数(int().min(0)),filename、mimeType不能为空(min(1));上传入口同时开始拒收小数字节数(400 INVALID_REQUEST)。size从 200 变成 400,正常客户端发的File.size本来就是整数。回滚即 revert 本 PR。packages/spec/src/api/storage.zod.ts(三处收窄)、packages/services/service-storage/src/storage-routes.ts(isDeclaredByteCount改用Number.isInteger)、.changeset/27598-presigned-size-whole-non-negative.md(BREAKING 说明与 ADR-0087 处置)。pnpm --filter @objectstack/spec exec vitest run src/api/storage.test.ts,预期 73 个测试全过,其中包括 “refuses a fractional size, naming size”。What changes
GetPresignedUrlRequestSchema.sizeisz.number().int().min(0), and its description reads "File size in bytes: a non-negative whole number (0 is an empty file)". The published JSON Schema now types itintegerwithminimum: 0.GetPresignedUrlRequestSchema.filenameand.mimeTypedeclaremin(1), described "(non-empty)". The order allowed this only if the door refuses''today. Measured onmain2e955dde: the door's presence check!filename || !mimeType || size == null(storage-routes.ts:968) answers400 INVALID_REQUESTfor an empty value of either key. The new door test below pins that reading.POST /api/v1/storage/upload/presigned):isDeclaredByteCountasksNumber.isInteger(size)in place ofNumber.isFinite(size), so a fractionalsizeis refused400 INVALID_REQUESTbefore anysys_filerow is written. The refusal message now says "a non-negative whole number". Measured onmain2e955dde:size: 1.5answered200, minted an upload URL and wrote a pending row. That row could never commit, because the complete door compares the held byte count (always whole) with the declared one (heldBytesRefusal,storage-routes.ts). Without this change the narrowed declaration would refuse a value the door still accepts, which is the same declared-versus-enforced gap in the other direction.content/docs/references/api/storage.mdxis regenerated (3 lines).27598-presigned-size-whole-non-negative.md:@objectstack/specpatch and@objectstack/service-storagepatch (the declared kind isnone: a REST request declaration that no metadata type embeds is not authorable metadata, the seat's claim correction 6524182832 on storage:GetPresignedUrlRequestSchema.sizedeclares no lower bound; narrow it toint().min(0)(#27549 part b) #27598),fix(spec)!, both declaration lines, and the ADR-0087 dispositionnot-required (no-migration-prescription).The door still does not parse its body with the schema. Its presence check and byte-count check run before the scope gate, as #22470 requires, so status, code, message and ordering stay as they were for every value other than a fraction.
Tests
packages/spec/src/api/storage.test.ts:refuses %s, naming sizecovers a negative size, a fractional size and a negative fraction. Each assertssuccess: false, exactly one issue, andpathequal to['size'].refuses an empty %s, naming that keycoversfilenameandmimeType, withcodetoo_small.CONTROL: size %d parses (0 is an empty file)covers0,1and1048576.packages/services/service-storage/src/storage-routes.test.ts:a positive fraction(1.5). Every row now also asserts thatGetPresignedUrlRequestSchema.safeParserefuses the same body.REFUSED 400 INVALID_REQUEST: an empty %s, which the declared schema refuses too, with no row writtencoversfilenameandmimeTypeon this door. No earlier test sent an empty name or type here.CONTROLrows (0,1024) assert that the declaration accepts the body too.Ablations: each narrowing turns its own case red
Each run used
node scripts/ablation-replace.mjs. The anchor hit once, and the file was restored to its HEAD blob withgit diff HEADempty.size: z.number().int().min(0)changed toz.number().min(0):× refuses a fractional size, naming size, 1 failed / 72 passed (73)..min(0)removed fromsize:× refuses a negative size, naming size, 1 failed / 72 passed (73). The negative fraction stays refused byint().filenamemin(1)removed:× refuses an empty filename, naming that key, 1 failed / 72 passed (73).mimeTypemin(1)removed:× refuses an empty mimeType, naming that key, 1 failed / 72 passed (73).Number.isInteger(size)changed back toNumber.isFinite(size):× REFUSED 400 INVALID_REQUEST: size as a positive fraction, with no sys_file row written, 1 failed / 74 passed (75). Before the door change, the same case failed with the door's200body, which is the measurement ofmainquoted above.Local runs
The package runs below were taken on
bbd24a5f/6e328c49(before the merge withorigin/main). The storage suites were re-run on the final head902d10c0: specstorage.test.tsTests 73 passed (73);@objectstack/service-storageTest Files 71 passed (71), Tests 1314 passed (1314).pnpm --filter @objectstack/spec test: Test Files 756 passed (756), Tests 22518 passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/service-storage test: Test Files 71 passed (71), Tests 1314 passed (1314).pnpm --filter @objectstack/service-storage typecheck: exit 0, andcheck:test-typecheckOK.pnpm --filter @objectstack/spec check:generated: 1 of 14 stale (content/docs/references/**) before--fix. After--fixall 14 are up to date.node scripts/check-adr-0087-registration.mjs: 1 declared-breaking changeset, carryingnot-required (no-migration-prescription).Acceptance notes
storage.upload(packages/client/src/index.ts) sendsmimeType: file.type. A browserFileof unknown type hastype === '', and the presigned door refuses that with400 INVALID_REQUEST, so such a file cannot be uploaded through the SDK helper. This PR now also declares that refusal. The fix belongs in the client (sendapplication/octet-stream), not in the door. Not measured end to end in a browser.filename/mimeTypeis still admitted by both the schema and the door. The two agree, and this PR leaves that alone, as fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty #27887 did.Clause-②: no. The order and triage6500594085writeno (narrowing), andcheck-changeset-no-majortreatsnarrowedbeside a line without the(narrowing)arm as a contradiction. This body and the changeset carryno (narrowing).Verification
All readings below are on head
902d10c0, the branch merged withorigin/mainat8cb8c7f461, after a fullpnpm turbo run build(116/116 tasks).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 109 commands (the same 109 before and after the merge). All 109 were run with stdin from /dev/null and the exit code captured before any pipe: 109 exited 0. On the pre-merge head, 4 first refused with exit 3 (PREREQUISITE NOT MET: no workspace build, and one self-test that needs an unshallow clone); after the build andgit fetch --unshallowthey exited 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE: "109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED".pnpm --filter @objectstack/spec check:generatedon the merged head: all 14 generated artefacts up to date.(narrowing)arm),pnpm --filter @objectstack/dogfood testunder the verify lock on902d10c0: Test Files 1 failed | 357 passed | 3 skipped (361), Tests 1 failed | 2882 passed | 20 skipped (2903). The one failure is not this change.test/api-action-door-forward.dogfood.test.ts> "the switch holds: ban_user OFF answers 409 ACTION_DISABLED through the door and the user is NOT banned" expects theACTION_DISABLEDmessage to containsys_metadata_activation, and the message is now the plain sentence "This action is turned off for this deployment. ..." (packages/objectql/src/action-activation.ts). Both the test and the new sentence are already onorigin/main, and this PR touches neither file nor anything they import from storage. Reported to the seat as a main-red candidate.Generated by Claude Code