Skip to content

types: sendError's extra cannot carry declaredCode, so every nested-envelope route silently drops an unregistered producer's own code spelling #11719

Description

@os-zhuang

Filed unassigned while implementing the #11683 + #11684 fold. Out of scope there: the repair is in packages/types, outside that card's declared file surface.

Measured

ApiErrorSchema (packages/spec/src/api/contract.zod.ts) declares declaredCode:

declaredCode: z.string().optional().describe('The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106)')

The shared nested-envelope writer cannot emit it. packages/types/src/response-envelope.ts:

exportfunctionsendError(res: EnvelopeResponse,status: number,code: ErrorCode,message: string,extra?: Pick<ApiError,'category'|'httpStatus'|'details'|'requestId'>,): void

declaredCode is not in that Pick, so no route answering through sendError can put it on the wire — it is a compile error to try.

Why it matters

ADR-0112's 2026-08-17 amendment (#9106, extended to the flat door by #9232) rules the demote platform-wide, in its own words: "error.code stays a closed vocabulary at every door. A thrown code that is not a member … is demoted to a declared sibling field, ApiError.declaredCode" — and it names the capability the channel exists to preserve: SandboxError carries a metadata app's own.code across the QuickJS boundary on purpose (#7867), so the author's spelling "still crosses the sandbox and still reaches the wire; it lands in the open channel rather than the closed one."

On a route answering the nested envelope it does not. The closed code still carries the derived member, so nothing invalid ships — the loss is silent and one-directional: the author's spelling is gone, and a consumer told by the ADR to read declaredCode finds nothing there.

Observed while routing the record-share family's refusals through the shared classification door: resolveThrownHttpError / demotedDeclaredCode produce the pair, the flat /data door carries both, and respondError (→ sendError) can only carry one. A sandboxed hook on a sys_record_share write is the reachable producer of a tenant-authored code there.

Not established here

  • The blast radius. The seven modules on the shared pair (storage-routes, settings-routes, admin-routes, external-datasource-routes, package-routes, i18n-service-plugin, share-link-routes) plus the record-share family all answer through sendError; which of them can actually catch a tenant-authored code was not censused.
  • Whether the fix is widening the Pick or routing these bodies through resolveThrownHttpError wholesale.
  • Severity not judged.

Region: packages/types/src/response-envelope.ts, sendError's extra parameter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions