You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chore(guard): extend the route-envelope check to the dispatcher domains (#4038 follow-up) (#4056)
The platform answers REST from two kinds of file. Route modules (*-routes.ts)
call `res.json(...)`, which this scan counted. Dispatcher domains
(runtime/src/domains/*.ts) RETURN `{ status, body }` for a central sender, so they
never call `res.json` and were invisible to it by construction.
That gap cost something real: /share-links emitted its payload under both `data`
and a legacy `link`/`links` for as long as nothing looked, and it surfaced only
because #3983's consumer sweep happened to walk past it (#4038, fixed in #4049).
I noted the blind spot there; this closes it.
The new table counts hand-built `response: { … }` literals per domain. A domain
answering only through the deps.* helpers hand-builds zero and cannot drift; any
hand-built one now needs a declared count plus a `note` saying why. Hand-building
is not automatically wrong — four kinds show up and only the last is drift:
1. enveloped, but the helper cannot express it — deps.success hardcodes 200 so a
201 is assembled by hand, and deps.error carries no headers so a 405 with
`Allow:` is too (keys.ts, share-links.ts, one in mcp.ts)
2. passthrough of a body this dispatcher does not own (mcp.ts, ai.ts)
3. a foreign wire format a client library requires — /auth answers better-auth's
shapes because better-auth's client parses them (auth.ts x4)
4. drift
16 domains audited: 11 helper-only, 5 declaring hand-built responses, 1 ratcheted.
The ratchet is ai.ts -> #4053, where GET /ai/agents is SDK-addressable but
unenveloped and the SDK compensates by reading `.agents` off the raw body —
converting it without changing cloud and the SDK together makes
`client.ai.agents.list()` return an empty list silently, which is indistinguishable
from the legitimate "no AI service configured" state. That warning now fails CI for
whoever tries it, instead of sitting in an issue nobody reads.
Five self-test assertions cover the new scan. One caught a real false positive
while it was being written: matching the `response` key alone counted a PAYLOAD
field named response (`deps.success({ response: … })`) as a hand-built response.
It now requires `response` to be a sibling of `handled`, which is what makes the
pair an HttpDispatcherResult rather than just an object with that key.
Verified the guard actually bites: adding a hand-built response to a helper-only
domain fails with the site and the four-kind classification prompt.
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z
Co-authored-by: Claude <noreply@anthropic.com>
// Kind 1 — enveloped, hand-built only because the helper cannot say it.
211
+
'keys.ts': {
212
+
handBuilt: 1,
213
+
note: 'POST /keys is a 201 and `deps.success` hardcodes 200; the body is the declared envelope',
214
+
},
215
+
'share-links.ts': {
216
+
handBuilt: 1,
217
+
note: 'POST /share-links is a 201, same reason as /keys (#4038 removed the duplicate key it also carried)',
218
+
},
219
+
220
+
// Kinds 1 and 2 together.
221
+
'mcp.ts': {
222
+
handBuilt: 2,
223
+
note: 'one 405 that must carry an `Allow:` header (`deps.error` takes none) and emits the declared envelope; one passthrough of the upstream MCP Web Response',
224
+
},
225
+
226
+
// Kind 3 — a foreign wire format, all four in the mock/fallback path.
227
+
'auth.ts': {
228
+
handBuilt: 4,
229
+
note: "bridges better-auth, whose client parses ITS shapes (`{ user, session }`, `{ session: null, user: null }`, `{ success: true }`) — BaseResponseSchema does not govern them",
230
+
},
231
+
232
+
// Kind 4 — real drift, tracked.
233
+
'ai.ts': {
234
+
handBuilt: 2,
235
+
ratchet: '#4053',
236
+
note: 'one passthrough of the AI service result (kind 2); one is the unenveloped `{ agents: [] }` fallback for GET /ai/agents — an SDK-addressable route whose conversion must land with cloud and the SDK together or `ai.agents.list()` silently returns []',
237
+
},
238
+
};
239
+
141
240
/**
142
241
* Count the envelope-relevant facts in one module's source.
0 commit comments