Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/hono-adapter-discovery-envelope.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
"@objectstack/hono": minor
---

feat(adapters): the hono adapter's two discovery bodies join the response envelope (#9436)

<!-- adr-0087: not-required (no-migration-prescription) Additive wire change:
`GET {prefix}` and `GET {prefix}/discovery` gain `success: true` beside the
existing `data` key — nothing authorable and no key is renamed, retired or
removed, so there is no conversion to register. Every measured reader
(`@objectstack/client` `connect()`'s `body.data || body`, the QA http-adapter's
`'routes' in body` discriminator, objectui's `typeof body.success === 'boolean'
&& 'data' in body` unwrap) resolves the new shape to the same document. -->

`GET {prefix}` and `GET {prefix}/discovery` answered `{ data: <discovery> }`
with no `success` flag — one key short of the declared `BaseResponseSchema`
envelope. They now answer `{ success: true, data: <discovery> }`.

Maintainer ruling on #9436 (2026-08-18, option A), deliberately not inheriting
#9389's pre-auth exemption: these bodies are read by SDKs, codegen and AI
clients — the envelope's core constituency — rather than by our own shells,
and the migration is one key. Readers that unwrapped `body.data` keep working
unchanged; envelope-aware readers that discriminate on `success` now unwrap
this mount correctly.
6 changes: 6 additions & 0 deletions packages/adapters/hono/src/hono.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,6 +91,7 @@ describe('createHonoApp', () => {
const res = await app.request('/api');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(json.data.version).toBe('1.0');
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
Expand All@@ -100,6 +101,7 @@ describe('createHonoApp', () => {
const res = await app.request('/api/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
});
Expand All@@ -109,6 +111,7 @@ describe('createHonoApp', () => {
const res = await customApp.request('/v2');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
});
Expand All@@ -118,6 +121,7 @@ describe('createHonoApp', () => {
const res = await customApp.request('/v2/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
});
Expand DownExpand Up@@ -620,6 +624,7 @@ describe('createHonoApp', () => {
const res = await outerApp.request('/api/v1');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
});
Expand All@@ -630,6 +635,7 @@ describe('createHonoApp', () => {
const res = await outerApp.request('/api/v1/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.success).toBe(true);
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
});
Expand Down
10 changes: 8 additions & 2 deletions packages/adapters/hono/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -290,12 +290,18 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
// ─── Explicit routes (framework-specific handling required) ────────────────

// --- Discovery ---
//
// Enveloped (`{ success: true, data }`) by maintainer ruling on #9436
// (2026-08-18, option A) — deliberately NOT inheriting #9389's pre-auth
// exemption: these bodies are read by SDKs, codegen and AI clients (the
// envelope's core constituency, not our own shells), and the migration was
// one key. The SDK's `connect()` unwraps `body.data || body` either way.
app.get(prefix, async (c) => {
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
return c.json({ success: true, data: await dispatcher.getDiscoveryInfo(prefix) });
});

app.get(`${prefix}/discovery`, async (c) => {
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
return c.json({ success: true, data: await dispatcher.getDiscoveryInfo(prefix) });
});

// --- .well-known ---
Expand Down
38 changes: 25 additions & 13 deletions scripts/check-route-envelope.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -535,13 +535,17 @@ const DISPATCHER_DOMAINS = {
* (`plugin-hono-server/src/adapter.ts`, `adapters/hono/src/index.ts`,
* `cli/src/commands/serve.ts`) are ordinary refusals at ordinary doors, and the
* ruling never reached them. #9364 converted them instead, which is what a
* ratchet is FOR and the visible contrast with this block: the first two are
* conformant above, and what survives in `adapters/hono/src/index.ts` is its two
* `{ data }` discovery bodies — pre-auth like this block, but read by SDKs and
* codegen rather than by our own shells, so #9389's closed three-file list does
* not reach them either and #9436 carries that question. The rejected option
* (A: envelope them, flip objectui's readers, carry a skew window on the least
* versionable seam in the product) is on record in #9389 rather than lost.
* ratchet is FOR and the visible contrast with this block: all three are
* conformant above. `adapters/hono/src/index.ts`'s last two counters — its
* `{ data }` discovery bodies, pre-auth like this block but read by SDKs and
* codegen rather than by our own shells — carried the SAME fork on a different
* consumer population, and the maintainer ruled it the OTHER way (#9436,
* 2026-08-18, option A: envelope them). The two rulings are one boundary read
* from both sides: WHO reads the body decides. Our own shells, pre-auth, high
* migration cost → exempt (here); SDKs/codegen/AI clients, one-key migration →
* envelope (#9436). The option #9389 rejected (A: envelope the SPA surfaces,
* flip objectui's readers, carry a skew window on the least versionable seam
* in the product) is on record in #9389 rather than lost.
*
* The reason is the deliverable. The entry is only where it is written down.
*
Expand DownExpand Up@@ -604,17 +608,25 @@ const PLUGIN_ROUTE_MODULES = {
// slot and `hostname` under `error.details`.
'packages/cli/src/commands/serve.ts': {},

// Converted by #9436 (maintainer ruling 2026-08-18, option A): the two
// `{ data }` discovery bodies gained `success: true`. This file's
// `errorCodeNotString 1` had already been removed by #9364 (the shared
// `errorJson` wrote the HTTP status into `error.code` and now derives the
// ADR-0112 member from it through `resolveThrownHttpError`), so this was the
// file's last counter. The ruling deliberately did NOT extend #9389's
// pre-auth exemption here: these bodies are read by SDKs and codegen, not by
// our own shells, and the migration was one key.
'packages/adapters/hono/src/index.ts': {},

// ── Ratchet: real, tracked, NOT blessed ─────────────────────────────────
//
// Measured by #9267 when this surface was added, not chosen. Each entry names
// the issue that will drive it to zero; every number ticks DOWN only. These
// are the finding this surface was worth adding for — none of them was
// visible to any check in the repo before it.
'packages/adapters/hono/src/index.ts': {
unenveloped: 2,
ratchet: '#9436 (envelope the hono adapter discovery bodies; Blocked-by #9389)',
note: 'two `{ data }` discovery bodies with no `success`. #9364 removed this file\'s `errorCodeNotString 1` — the shared `errorJson` wrote the HTTP status into `error.code` and now derives the ADR-0112 member from it through `resolveThrownHttpError`. What is left is the same PRE-AUTH bare-payload fork #9389 rules on, but on a different consumer population (SDKs and codegen read this mount\'s discovery, not the Console SPA), so #9389\'s closed three-file list does not reach it',
},
// visible to any check in the repo before it. #9436 graduated the last
// ratcheted member (`adapters/hono`, above); the section stays because the
// next measured drift lands here. `trigger-api` below was measured clean
// when the surface was added and is a pinned zero, not a ratchet.
'packages/triggers/trigger-api/src/plugin.ts': {},

// ── Ruled exempt: the pre-auth bootstrap seam (#9389) ────────────────────
Expand Down
Loading