Observation-class finding, recorded while fixing #7981 (PR #8071). Not fixed there — that card was scoped to the three /security/suggested-bindings routes by explicit ruling, and these are separate registrars. Filed unassigned.
Fact
With #7981 landed, registerSecurityEndpoints speaks the ADR-0112 D5 envelope { error: { code, message } } on every arm. Its two immediate neighbours in packages/rest/src/rest-server.ts do not, and they carry the same two dialects #7035 (PR #7293) retired from this file's /meta refusals:
| registrar | routes | arms still on a retired dialect |
|---|
registerSecurityExplainEndpoints | GET/POST /api/v1/security/explain, GET /api/v1/security/my-delegable-scope | 401 UNAUTHORIZED, 501 NOT_IMPLEMENTED, 400 VALIDATION_FAILED, 403 PERMISSION_DENIED are all flat { code, message }; the two 500s are { code, error: 'a bare string' } |
registerSharingEndpoints (respondSharingError) | the /sharing/* family | { code, error: msg.replace(...) } — bare-string error, one emitter feeding 400 / 403 / 404 / 409 / 422 |
So body.error.code, the one position ADR-0112 D5 declares, reads undefined on all of them, and /security/explain is directly adjacent to the routes that no longer behave that way — a client calling explain and then suggested-bindings now meets two shapes across one security family.
Why it is worth recording separately
registerSecurityExplainEndpoints is not dormant: the client SDK exposes it as client.security.explain and client.security.describeDelegableScope (packages/client/src/index.ts), and content/docs/permissions/* documents the route for operators. security-routes.test.ts currently PINS the flat shape (expect(res.body.code).toBe('NOT_IMPLEMENTED'), ...('PERMISSION_DENIED'), ...('EXPLAIN_FAILED')), so those pins move with any convergence — the same fixture-triage step #7981 needed.
The same wire-visibility question #7981 had applies and has the same measured answer: the SDK's error path in ObjectStackClient.fetch reads BOTH envelopes' declared spots (errorBody?.code ?? errorBody?.error?.code, and a typeof errorBody?.error === 'string' limb for the message), so a converged body keeps producing identical err.code / err.message. That measurement is recorded on #7981 and should be re-run, not assumed, for whatever consumers these two families have.
Not claimed here
Suggested handling
Triage-level, not decided here: converge each registrar onto { error: { code, message } } through a single emitter, exactly as PR #8071 did for registerSecurityEndpoints — the shape is already ruled by ADR-0112 D5, so this is implementation of an existing decision. The explain pair is the smaller, higher-adjacency half and is a natural first card.
Generated by Claude Code
Observation-class finding, recorded while fixing #7981 (PR #8071). Not fixed there — that card was scoped to the three
/security/suggested-bindingsroutes by explicit ruling, and these are separate registrars. Filed unassigned.Fact
With #7981 landed,
registerSecurityEndpointsspeaks the ADR-0112 D5 envelope{ error: { code, message } }on every arm. Its two immediate neighbours inpackages/rest/src/rest-server.tsdo not, and they carry the same two dialects #7035 (PR #7293) retired from this file's/metarefusals:registerSecurityExplainEndpointsGET/POST /api/v1/security/explain,GET /api/v1/security/my-delegable-scopeUNAUTHORIZED, 501NOT_IMPLEMENTED, 400VALIDATION_FAILED, 403PERMISSION_DENIEDare all flat{ code, message }; the two 500s are{ code, error: 'a bare string' }registerSharingEndpoints(respondSharingError)/sharing/*family{ code, error: msg.replace(...) }— bare-stringerror, one emitter feeding 400 / 403 / 404 / 409 / 422So
body.error.code, the one position ADR-0112 D5 declares, readsundefinedon all of them, and/security/explainis directly adjacent to the routes that no longer behave that way — a client callingexplainand thensuggested-bindingsnow meets two shapes across onesecurityfamily.Why it is worth recording separately
registerSecurityExplainEndpointsis not dormant: the client SDK exposes it asclient.security.explainandclient.security.describeDelegableScope(packages/client/src/index.ts), andcontent/docs/permissions/*documents the route for operators.security-routes.test.tscurrently PINS the flat shape (expect(res.body.code).toBe('NOT_IMPLEMENTED'),...('PERMISSION_DENIED'),...('EXPLAIN_FAILED')), so those pins move with any convergence — the same fixture-triage step #7981 needed.The same wire-visibility question #7981 had applies and has the same measured answer: the SDK's error path in
ObjectStackClient.fetchreads BOTH envelopes' declared spots (errorBody?.code ?? errorBody?.error?.code, and atypeof errorBody?.error === 'string'limb for the message), so a converged body keeps producing identicalerr.code/err.message. That measurement is recorded on #7981 and should be re-run, not assumed, for whatever consumers these two families have.Not claimed here
respondSharingError'smsg.startsWith(CODE)string-prefix protocol should survive at all — it is a second, unrelated question (the code is recovered by parsing the message text) and is not part of the envelope position.registerSecurityEndpointsanswers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981, this is about body shape only.registerSharingEndpointsis a wider blast radius than the explain pair and may deserve its own card.Suggested handling
Triage-level, not decided here: converge each registrar onto
{ error: { code, message } }through a single emitter, exactly as PR #8071 did forregisterSecurityEndpoints— the shape is already ruled by ADR-0112 D5, so this is implementation of an existing decision. The explain pair is the smaller, higher-adjacency half and is a natural first card.Generated by Claude Code