Repository navigation
Commit 2c683cf
fix(spec)!: the presigned upload request declares a whole, non-negative size and a non-empty filename and mimeType (#28136)
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao9ZLkgazHGbBtMatjZpT18B)_
Fixes #27598
Clause-②: no (narrowing)
Metadata-protocol: none
## 维护者速读
- 改了什么:预签名上传请求的 schema(`GetPresignedUrlRequestSchema`)现在声明 `size`
必须是非负整数(`int().min(0)`),`filename`、`mimeType`
不能为空(`min(1)`);上传入口同时开始拒收小数字节数(400 INVALID_REQUEST)。
- 为什么改:公开 schema 原来接受负数、小数和空字符串,入口却拒收负数和空值,按 schema 写代码的调用方或 AI
会被误导;小数字节数更糟,入口会发出上传地址并写一条永远无法完成的待上传记录。
- 风险与回滚:负数、非数字、空文件名/类型的入口行为一字未改;唯一的行为变化是小数 `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.size` is `z.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 it `integer`
with `minimum: 0`.
- **Declared increment (same file, same declared-versus-enforced class,
the #27887 precedent):** `GetPresignedUrlRequestSchema.filename` and
`.mimeType` declare `min(1)`, described "(non-empty)". The order allowed
this only if the door refuses `''` today. Measured on `main` `2e955dde`:
the door's presence check `!filename || !mimeType || size == null`
(`storage-routes.ts:968`) answers `400 INVALID_REQUEST` for an empty
value of either key. The new door test below pins that reading.
- **Door (`POST /api/v1/storage/upload/presigned`):**
`isDeclaredByteCount` asks `Number.isInteger(size)` in place of
`Number.isFinite(size)`, so a fractional `size` is refused `400
INVALID_REQUEST` before any `sys_file` row is written. The refusal
message now says "a non-negative whole number". Measured on `main`
`2e955dde`: `size: 1.5` answered `200`, 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.mdx` is regenerated (3 lines).
- Changeset `27598-presigned-size-whole-non-negative.md`:
`@objectstack/spec` patch and `@objectstack/service-storage` patch (the
declared kind is `none`: a REST request declaration that no metadata
type embeds is not authorable metadata, the seat's claim correction
6524182832 on #27598), `fix(spec)!`, both declaration lines, and the
ADR-0087 disposition `not-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 size` covers a negative size, a fractional size
and a negative fraction. Each asserts `success: false`, exactly one
issue, and `path` equal to `['size']`.
- `refuses an empty %s, naming that key` covers `filename` and
`mimeType`, with `code` `too_small`.
- `CONTROL: size %d parses (0 is an empty file)` covers `0`, `1` and
`1048576`.
- `packages/services/service-storage/src/storage-routes.test.ts`:
- The #27413 refusal table gains `a positive fraction` (`1.5`). Every
row now also asserts that `GetPresignedUrlRequestSchema.safeParse`
refuses the same body.
- `REFUSED 400 INVALID_REQUEST: an empty %s, which the declared schema
refuses too, with no row written` covers `filename` and `mimeType` on
this door. No earlier test sent an empty name or type here.
- The `CONTROL` rows (`0`, `1024`) assert that the declaration accepts
the body too.
<details><summary>Ablations: each narrowing turns its own case
red</summary>
Each run used `node scripts/ablation-replace.mjs`. The anchor hit once,
and the file was restored to its HEAD blob with `git diff HEAD` empty.
- `size: z.number().int().min(0)` changed to `z.number().min(0)`: `×
refuses a fractional size, naming size`, 1 failed / 72 passed (73).
- `.min(0)` removed from `size`: `× refuses a negative size, naming
size`, 1 failed / 72 passed (73). The negative fraction stays refused by
`int()`.
- `filename` `min(1)` removed: `× refuses an empty filename, naming that
key`, 1 failed / 72 passed (73).
- `mimeType` `min(1)` removed: `× refuses an empty mimeType, naming that
key`, 1 failed / 72 passed (73).
- Door `Number.isInteger(size)` changed back to `Number.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's `200` body, which is the
measurement of `main` quoted above.
</details>
<details><summary>Local runs</summary>
The package runs below were taken on `bbd24a5f` / `6e328c49` (before the
merge with `origin/main`). The storage suites were re-run on the final
head `902d10c0`: spec `storage.test.ts` Tests 73 passed (73);
`@objectstack/service-storage` Test 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, and
`check:test-typecheck` OK.
- `pnpm --filter @objectstack/spec check:generated`: 1 of 14 stale
(`content/docs/references/**`) before `--fix`. After `--fix` all 14 are
up to date.
- `node scripts/check-adr-0087-registration.mjs`: 1 declared-breaking
changeset, carrying `not-required (no-migration-prescription)`.
</details>
## Acceptance notes
- **Out of scope, reported to the seat and not filed:** the SDK's
`storage.upload` (`packages/client/src/index.ts`) sends `mimeType:
file.type`. A browser `File` of unknown type has `type === ''`, and the
presigned door refuses that with `400 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 (send
`application/octet-stream`), not in the door. Not measured end to end in
a browser.
- A whitespace-only `filename` / `mimeType` is still admitted by both
the schema and the door. The two agree, and this PR leaves that alone,
as #27887 did.
- The claim writes `Clause-②: no`. The order and triage `6500594085`
write `no (narrowing)`, and `check-changeset-no-major` treats `narrowed`
beside a line without the `(narrowing)` arm as a contradiction. This
body and the changeset carry `no (narrowing)`.
## Verification
All readings below are on head `902d10c0`, the branch merged with
`origin/main` at `8cb8c7f461`, after a full `pnpm turbo run build`
(116/116 tasks).
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 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 and
`git fetch --unshallow` they 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:generated` on the merged head:
all 14 generated artefacts up to date.
- Full dogfood (owed by the `(narrowing)` arm), `pnpm --filter
@objectstack/dogfood test` under the verify lock on `902d10c0`: 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 the `ACTION_DISABLED` message to contain
`sys_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 on `origin/main`, and this PR touches neither
file nor anything they import from storage. Reported to the seat as a
main-red candidate.
- CI on this PR: not yet read; the seat reads the check-runs.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvsQx1XxK9FrejoXno)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 10fc9bf commit 2c683cf
6 files changed
Lines changed: 101 additions & 17 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- services/service-storage/src
- spec/src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
224 | | - | |
225 | | - | |
226 | | - | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
133 | 137 | | |
134 | 138 | | |
135 | 139 | | |
136 | 140 | | |
137 | 141 | | |
138 | 142 | | |
| 143 | + | |
139 | 144 | | |
140 | | - | |
| 145 | + | |
141 | 146 | | |
142 | 147 | | |
143 | 148 | | |
144 | 149 | | |
145 | 150 | | |
| 151 | + | |
146 | 152 | | |
147 | 153 | | |
148 | 154 | | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
149 | 174 | | |
150 | 175 | | |
| 176 | + | |
151 | 177 | | |
152 | | - | |
| 178 | + | |
153 | 179 | | |
154 | 180 | | |
| 181 | + | |
155 | 182 | | |
156 | 183 | | |
157 | 184 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
171 | 173 | | |
172 | 174 | | |
173 | | - | |
| 175 | + | |
174 | 176 | | |
175 | 177 | | |
176 | 178 | | |
177 | | - | |
| 179 | + | |
178 | 180 | | |
179 | 181 | | |
180 | 182 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
81 | 113 | | |
82 | 114 | | |
83 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
69 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
70 | 73 | | |
71 | 74 | | |
72 | 75 | | |
| |||
0 commit comments