Surfaced while consolidating the seven envelope builders onto one shared pair (#3973). Filed rather than folded in, because fixing it is a wire change and #3973 was explicitly a consolidation that changes no bytes.
The finding
settings-routes.ts passes ad-hoc keys as SIBLINGS of code and message inside error:
sendError(res, 403, 'SETTINGS_FORBIDDEN', err.message, { namespace: err.namespace });
sendError(res, 409, 'SETTINGS_LOCKED', err.message, { namespace, key, reason });
sendError(res, 400, 'UNKNOWN_KEY', err.message, { namespace, key });
sendError(res, 400, 'SETTINGS_VALIDATION', err.message, { namespace, fields });
ApiErrorSchema (packages/spec/src/api/contract.zod.ts) declares code, message, category?, httpStatus?, details?, requestId?. namespace / key / reason / fields are none of those. Because the schema is a plain z.object, unknown keys are stripped rather than rejected — so these bodies are conformant by stripping, not by declaration, and envelopeViolations (which only inspects the body's top level) does not see them either.
The same module already uses the declared slot correctly one branch over:
sendError(res, 400, 'SETTINGS_ACTION_FAILED', result.message ?? '…', { details: result });
so this is an inconsistency inside one file, not a missing capability.
Why it is not urgent
Nothing in the repo reads error.namespace / error.key / error.reason / error.fields — grep across packages/, examples/ finds no consumer. They are write-only today.
Why it is worth closing anyway
It is the reason the shared sendError types its last parameter Record<string, unknown> rather than the declared optional fields of ApiError:
// packages/types/src/response-envelope.ts
extra?: Record<string, unknown>,
That is the one loose edge on an otherwise closed builder — the same builder that now types code as the closed ADR-0112 ErrorCode union. Move settings' four call sites under details and the parameter can be tightened to the declared fields, which closes the envelope's error half the way #3841 closed its vocabulary.
Decision needed
Whether error.details is the right home (it is the declared slot, and the module already uses it), or whether these belong in the fields[] field-level vocabulary that ADR-0112 D6 / #3977 carve out — SETTINGS_VALIDATION in particular passes fields, which reads like the latter.
Either way it is a change to what /api/settings/* puts on the wire for four error branches, so it wants a changeset with the FROM → TO mapping.
Surfaced while consolidating the seven envelope builders onto one shared pair (#3973). Filed rather than folded in, because fixing it is a wire change and #3973 was explicitly a consolidation that changes no bytes.
The finding
settings-routes.tspasses ad-hoc keys as SIBLINGS ofcodeandmessageinsideerror:ApiErrorSchema(packages/spec/src/api/contract.zod.ts) declarescode,message,category?,httpStatus?,details?,requestId?.namespace/key/reason/fieldsare none of those. Because the schema is a plainz.object, unknown keys are stripped rather than rejected — so these bodies are conformant by stripping, not by declaration, andenvelopeViolations(which only inspects the body's top level) does not see them either.The same module already uses the declared slot correctly one branch over:
so this is an inconsistency inside one file, not a missing capability.
Why it is not urgent
Nothing in the repo reads
error.namespace/error.key/error.reason/error.fields—grepacrosspackages/,examples/finds no consumer. They are write-only today.Why it is worth closing anyway
It is the reason the shared
sendErrortypes its last parameterRecord<string, unknown>rather than the declared optional fields ofApiError:That is the one loose edge on an otherwise closed builder — the same builder that now types
codeas the closed ADR-0112ErrorCodeunion. Move settings' four call sites underdetailsand the parameter can be tightened to the declared fields, which closes the envelope's error half the way #3841 closed its vocabulary.Decision needed
Whether
error.detailsis the right home (it is the declared slot, and the module already uses it), or whether these belong in thefields[]field-level vocabulary that ADR-0112 D6 / #3977 carve out —SETTINGS_VALIDATIONin particular passesfields, which reads like the latter.Either way it is a change to what
/api/settings/*puts on the wire for four error branches, so it wants a changeset with the FROM → TO mapping.