Skip to content

Commit ecb7b44

Browse files
fix(spec): describe CompleteUploadRequest.eTag as stored, not compared (#24695)
Closes #24648 Clause-②: no ## What changes `CompleteUploadRequestSchema.eTag` (`packages/spec/src/api/storage.zod.ts`) described itself as `S3 ETag verification`. No runtime path verifies it. The description now says what runs: > ETag the client reports for the uploaded object. Stored on the file record as sent and not compared with anything: no storage adapter reports a stored object's ETag. Before committing, the door checks that the storage backend holds bytes under the file's key and, when the upload declared a size, that the held size equals it. Route: amend the describe, per the triage lean and the claim. No shape change, no new check, no retirement. The key keeps the same type and optionality. ## Evidence the new sentence is accurate (read on `origin/main` 3a931fc) - `packages/services/service-storage/src/storage-routes.ts`, `POST /storage/upload/complete`: the body's `eTag` goes into `store.updateFile(fileId, { status: 'committed', etag: eTag ?? undefined })`. It is stored verbatim and never read before that write. - In the same file, `heldBytesRefusal` (added with PR #24643) runs before the write. It refuses when `storage.exists(file.key)` is false. When the row declares a size, it also refuses when `storage.getInfo(file.key).size` differs from that size. Its own docblock says the eTag is not compared, because no adapter's `getInfo` reports the stored object's ETag. - `StorageFileInfo` (`packages/spec/src/contracts/storage-service.ts`) has no `etag` member. ## Files - `packages/spec/src/api/storage.zod.ts`: the describe. - `content/docs/references/api/storage.mdx`: regenerated by `pnpm --filter @objectstack/spec check:generated --fix`, which proved only `check:docs` stale and ran `gen:docs`. Not edited by hand. - `.changeset/24648-etag-describe.md`: `@objectstack/spec` patch. No test pins the old sentence. The only occurrences of `S3 ETag verification` were the schema and the generated reference page. The key is not in any `packages/spec/liveness/*.json` ledger. The one hit in `qa.json` is the substring inside `parseTagsFlag`. No other open PR touches `storage.zod.ts` or `references/api/storage.mdx`; I read the file lists of all open PRs before opening this one. ## Verification (head 57b0e5a, after merging origin/main) - `pnpm --filter @objectstack/spec build`: exit 0. - `pnpm --filter @objectstack/spec typecheck`: exit 0, including the `check:test-typecheck` layer. - `pnpm --filter @objectstack/spec test`: 702 files passed, 21349 tests passed, 1 todo. - `pnpm --filter @objectstack/spec check:generated`: red before the regeneration (1 of 14 stale: `content/docs/references/**`). Green after `--fix`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: derived 104 commands. All 104 ran. 103 exit 0 (6 of them were re-run after building their declared prerequisites: `@objectstack/lint` and its closure, `@objectstack/formula`, `@objectstack/client-react`, `@objectstack/objectql`). - NOT MEASURED: `pnpm check:dual-build-cjs-loads`. Reason: it needs a whole-workspace `pnpm build` (29 or more packages have no `dist/`), and that build does not fit the local budget. The diff changes one describe string and no package entry point. Declared to CI. ## Acceptance notes - Enforcing the eTag would be a contract addition: an `etag` on `StorageFileInfo`, the S3 adapter reporting `HeadObject`'s `ETag`, and a compare at the commit door. Retiring the key would be a protocol change. Both are left out, per triage. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 41edf73 commit ecb7b44

3 files changed

Lines changed: 12 additions & 2 deletions

File tree

‎.changeset/24648-etag-describe.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`CompleteUploadRequestSchema.eTag` no longer describes itself as "S3 ETag verification". The upload completion door stores the client-reported value on the file record as sent and compares it with nothing, because no storage adapter reports a stored object's ETag. The description now says so, and names the check the door does run before committing: the storage backend must hold bytes under the file's key and, when the upload declared a size, hold exactly that many. The schema's shape is unchanged.

‎content/docs/references/api/storage.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data);
102102
| Property | Type | Required | Description |
103103
| :--- | :--- | :--- | :--- |
104104
| **fileId** | `string` | ✅ | File ID returned from presigned request |
105-
| **eTag** | `string` | optional | S3 ETag verification |
105+
| **eTag** | `string` | optional | ETag the client reports for the uploaded object. Stored on the file record as sent and not compared with anything: no storage adapter reports a stored object's ETag. Before committing, the door checks that the storage backend holds bytes under the file's key and, when the upload declared a size, that the held size equals it. |
106106

107107

108108
---

‎packages/spec/src/api/storage.zod.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ export const GetPresignedUrlRequestSchema = lazySchema(() => z.object({
7373

7474
export const CompleteUploadRequestSchema = lazySchema(() => z.object({
7575
fileId: z.string().describe('File ID returned from presigned request'),
76-
eTag: z.string().optional().describe('S3 ETag verification'),
76+
eTag: z.string().optional().describe(
77+
'ETag the client reports for the uploaded object. Stored on the file record as sent and not compared '
78+
+ 'with anything: no storage adapter reports a stored object\'s ETag. Before committing, the door checks '
79+
+ 'that the storage backend holds bytes under the file\'s key and, when the upload declared a size, '
80+
+ 'that the held size equals it.',
81+
),
7782
}));
7883

7984
// ==========================================

0 commit comments

Comments
 (0)