Skip to content

fix(service-storage): emit the declared success envelope on all eight routes (#3689) - #3837

Merged
os-zhuang merged 1 commit into
mainfrom
claude/service-storage-success-bodies-gnxyl9
Jul 28, 2026
Merged

fix(service-storage): emit the declared success envelope on all eight routes (#3689)#3837
os-zhuang merged 1 commit into
mainfrom
claude/service-storage-success-bodies-gnxyl9

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3689 — the other half of #3675.

Takes option 1 from the issue (normalize all three shapes), which is what the issue recommends and what packages/spec being the source of truth requires: the SDK's declared return types are inferred from these schemas, so leaving two routes as "declared exceptions" (option 2) or rewriting the schemas to match the drift (option 3) both keep a surface where the contract and the wire disagree.

The drift

Route(s) Was Now
/upload/presigned, /upload/complete, /upload/chunked, …/chunk/:i, …/complete, …/progress { data: {…} } { success: true, data: {…} }
GET /files/:fileId/url { url } { success: true, data: { url } }
PUT /_local/raw/:token { ok: true, key } { success: true, data: { key } }

storage.zod.ts declared every one of these as BaseResponseSchema.extend({ data }), and PresignedUrlResponse and friends are z.inferred from those schemas and published as the SDK's return types. The declaration said success: boolean; the wire said nothing. It broke nothing only because the storage SDK methods returned res.json() raw — any, so the compiler could not see the gap and nothing relied on the declaration. That is the posture i18n was in before #3636, right up until something did.

Why it needed its own PR

The payload moves on the last two, so this is not the find-replace the six upload routes would have been. Every in-repo consumer was fixed first, so the two repos are not coupled by merge order — the same approach the error path took:

  • client.storage.getDownloadUrl() now reads through unwrapResponse, the SDK's one standard envelope seam — it strips the envelope when present and returns the body untouched when not, so a client either side of this server change resolves the same URL. Deliberately not a bespoke ?? grown for this route (PD Add comprehensive test suite for Zod schema validation #12); every other enveloped SDK method already goes through it. The remaining storage methods hand back the whole envelope by design and were already correct.
  • The console's two attachment openers (RecordAttachmentsPanel, ApprovalsInboxPage) already read body?.url ?? body?.data?.url. objectui#2913 pins that as deliberate rather than incidental — it is tests and comments only, nothing to fix there.
  • attachments-permission-matrix.dogfood.test.ts read the bare .url and is updated.

ok is dropped, not kept beside success — it was a second, private word for the same thing, on a route mounted under /api/v1/storage like any other. Nothing reads that body: the SDK and the console both PUT there opaquely, exactly as they would an S3 presigned URL, and check only the status.

Two schemas that never existed

FileDownloadUrlResponse and RawUploadResponse are now declared, and getDownloadUrl joins StorageApiContracts — it had never been in the registry despite being ledgered disposition: 'sdk', and that absence is how its shape drifted outside the envelope unnoticed. The _local/raw/:token pair stays out of the registry on purpose: it is the local adapter's own presign loopback, ledgered server-only and addressed as an opaque signed URL rather than as an API, so it gets a declared shape without being promoted to a client contract.

The guard

success-envelope.conformance.test.ts holds the new shape in place the way error-envelope.conformance.test.ts holds the error one — same harness, same imported spec, three directions:

  1. every route is driven and its body parsed against the declared schema it answers to, imported rather than restated, so a body that parses is a body the published SDK type does not lie about;
  2. the retired shapes are asserted dead (no top-level url/ok/key), so a revert cannot pass quietly;
  3. the module source is scanned — exactly two .json( call sites, one success: true, one success: false, no ok: true — so a new route cannot bypass the single sendOk helper. Without (3) the suite would only ever cover the routes that existed the day it was written.

Mutation-checked: reverting one route to res.json({ url }) fails 3 of its tests.

As with #3675, the route ledgers cannot catch this class of drift — they audit which routes exist and whether the SDK can address them, not what comes back.

Not in scope

The two items the issue flags as related, not the same are untouched and still open questions: the dispatcher putting the HTTP status number in error.code, and the StandardErrorCode snake_case-vs-SCREAMING_SNAKE vocabulary split. Both are spec decisions, not envelope shape.

Verification

  • service-storage 222 ✅ (incl. the new 10-case suite) · client 179 ✅ · spec 6752 ✅ · runtime 665 ✅
  • check:docs, check:api-surface, check:spec-changes, check:upgrade-guide ✅ — storage.mdx, api-surface.json and the schema manifest regenerated and committed
  • eslint clean on every changed file
  • Changeset included, carrying the FROM → TO mapping for the two moved payloads

🤖 Generated with Claude Code

https://claude.ai/code/session_01RmnSQC8KxxsoZQe3oDEHjG


Generated by Claude Code

… routes (#3689)

The other half of #3675. That PR moved the ERROR bodies of the autonomously-
mounted `/api/v1/storage/*` routes into the declared
`{ success: false, error: { code, message } }` envelope and deliberately
stopped there: unlike the errors, the success bodies were not an additive fix.
They were three shapes, none carrying the `success` flag `BaseResponseSchema`
declares and `ObjectStackClient.unwrapResponse` keys on —

  the six upload routes    { data: {…} }     → { success: true, data: {…} }
  GET /files/:fileId/url   { url }           → { success: true, data: { url } }
  PUT /_local/raw/:token   { ok: true, key } → { success: true, data: { key } }

— while `storage.zod.ts` declared every one of them as
`BaseResponseSchema.extend({ data })`, and `PresignedUrlResponse` and friends
are z.inferred from those schemas and published as the SDK's return types. The
declaration said `success: boolean`; the wire said nothing. It broke nothing
only because the storage SDK methods returned `res.json()` raw — `any`, so the
compiler could not see the gap and nothing relied on the declaration. That is
the posture i18n was in before #3636, right up until something did.

The payload MOVES on the last two, which is why this was filed separately
rather than smuggled into #3675. Every in-repo consumer was fixed first, so the
repos are not coupled by merge order: `client.storage.getDownloadUrl()` now
reads through `unwrapResponse` — the SDK's one standard envelope seam, which
strips the envelope when present and returns the body untouched when not, so a
client either side of this server change resolves the same URL — and the
console's two attachment openers already read both `url` shapes (objectui
follow-up pins that). `ok` is dropped rather than kept beside `success`: it was
a second, private word for the same thing.

Two schemas that were missing are now declared, `FileDownloadUrlResponse` and
`RawUploadResponse`, and `getDownloadUrl` joins `StorageApiContracts`, which it
had never been in — that absence is how its shape drifted outside the envelope
unnoticed. The `_local/raw/:token` pair stays out of the registry on purpose:
it is the local adapter's own presign loopback, ledgered `server-only` and
addressed as an opaque signed URL rather than as an API.

`success-envelope.conformance.test.ts` holds the new shape in place the way its
error twin holds the other: every route is driven and its body parsed against
the DECLARED schema it answers to — not a restatement — the retired shapes are
asserted dead, and the module source is scanned so a new route cannot bypass
the single `sendOk` helper. As with #3675, the route ledgers cannot catch this
class of drift: they audit which routes exist and whether the SDK can address
them, not what comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmnSQC8KxxsoZQe3oDEHjG
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 9:02am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, packages/qa, packages/services, @objectstack/spec.

111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client)
  • content/docs/permissions/authorization.mdx (via packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service-storage success bodies are three shapes, none carrying success: true — the other half of #3675

2 participants