From 16a53137791a137d70fe8772ed2d535f44883765 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 02:22:36 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugin-hono-server):=20registerStandar?= =?UTF-8?q?dEndpoints=20defaults=20to=20false=20=E2=80=94=20the=20convenie?= =?UTF-8?q?nce=20surface=20is=20opt-in=20(#4073)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flag mounts raw C+R `/api/v1/data/:object` plus `/api/v1/discovery` / `/.well-known/objectstack`. Everything it mounts is duplicate — and lesser — supply: C+R only, a subset of the gates, a pre-`DiscoverySchema` discovery payload. REST serves full `/data` CRUD behind the whole gate stack, REST/the dispatcher own discovery (#4018 cede), and #4260 pinned that a composed host answers BYTE-IDENTICALLY with the flag on or off. The tax was real: #2567, #3298 and #4018 each re-implemented a platform invariant here after the fact. Who this changes, checked against who RELIES ON THE DEFAULT (the issue's first correction), not who passes the option: - composed hosts — `os serve` (serve.ts:1167), `objectstack dev` (dev-plugin.ts:386), every documented path: no change, REST/dispatcher already answered first (#4260); - bare hosts (none in-repo; external unknowns): the surface no longer mounts. Boot logs ONE warn naming the flag, #4073 and the remedy — mount `createRestApiPlugin` (same `objectql` dependency the raw surface already required, full CRUD + gates in return) or opt in explicitly. Absence is loud; a silent 404 is the failure shape this whole thread kept paying for. The warn stays quiet whenever REST or the dispatcher is mounted, so transport-only compositions are not nagged into cargo-culting the flag back; - `/me/*` — unaffected, never behind this flag (#4144). The default's behavior gets its own pin suite (`hono-standard-endpoints-default.test.ts`) rather than riding on the explicit-`false` tests: defaults are invisible at call sites, so a constructor drift would otherwise regress silently. Six pins: default mounts nothing of the surface but keeps `/me/*`; explicit opt-in still works; bare boot warns once with the remedy; REST-composed, dispatcher-composed and opted-in boots stay quiet. Three suites tested the legacy surface THROUGH the default and now opt in explicitly (hono-discovery, hono-anonymous-deny — server-timing-e2e already bypassed the flag by calling the registrar directly). The flag is marked `@deprecated` with the end state in its JSDoc: one release of observation, then `registerDiscoveryAndCrudEndpoints` and the flag are deleted and this plugin becomes a pure transport adapter (ADR-0076 D11). Verified: plugin-hono-server 155/155, runtime hono suites 23/23, client hono suites 16/16, http-conformance 46/46, package tsc clean, changed-file lint 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd --- .../hono-standard-endpoints-default-off.md | 36 ++++++ .../src/hono-anonymous-deny.test.ts | 8 +- .../src/hono-discovery.test.ts | 3 +- .../plugin-hono-server/src/hono-plugin.ts | 56 +++++++-- .../hono-standard-endpoints-default.test.ts | 116 ++++++++++++++++++ 5 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 .changeset/hono-standard-endpoints-default-off.md create mode 100644 packages/plugins/plugin-hono-server/src/hono-standard-endpoints-default.test.ts diff --git a/.changeset/hono-standard-endpoints-default-off.md b/.changeset/hono-standard-endpoints-default-off.md new file mode 100644 index 0000000000..170dd60e57 --- /dev/null +++ b/.changeset/hono-standard-endpoints-default-off.md @@ -0,0 +1,36 @@ +--- +"@objectstack/plugin-hono-server": minor +--- + +feat(plugin-hono-server): `registerStandardEndpoints` now defaults to `false` — the deprecated CRUD/discovery convenience surface is opt-in (#4073) + +The flag mounts raw C+R `/api/v1/data/:object` and `/api/v1/discovery` / +`/.well-known/objectstack`. Every path it mounts is duplicate — and lesser — +supply: C+R only, a subset of the gates, a pre-`DiscoverySchema` discovery +payload. `@objectstack/rest` serves full `/data` CRUD behind the whole gate +stack, REST/the dispatcher own discovery (#4018 cede), and #4260 pinned that a +composed host answers **byte-identically** with the flag on or off. The surface +has also been a standing tax: #2567, #3298 and #4018 each had to re-implement a +platform invariant here after the fact. + +**FROM → TO** + +- **Composed hosts (REST and/or the dispatcher mounted)** — `os serve`, + `objectstack dev`, cloud's objectos, every documented path: **no change**. + Those plugins already answer every route this surface covered, and answered + them first. +- **Bare hosts (HonoServerPlugin only)**: `/api/v1/data/:object`, + `/api/v1/discovery` and `/.well-known/objectstack` are **no longer mounted by + default**. The boot now logs a warn naming the flag and the remedy instead of + leaving a silent 404. Migrate by mounting `createRestApiPlugin` from + `@objectstack/rest` — it needs the same `objectql` service this surface + already required, and returns full CRUD plus the gate stack — or pass + `registerStandardEndpoints: true` to keep the legacy surface during the + deprecation window. +- The current-user endpoints (`/auth/me/permissions`, `/auth/me/localization`, + `/me/apps`) are **unaffected** — they never sat behind this flag (#4144) and + register unconditionally. + +The flag is now marked `@deprecated`. Next step per #4073: one release of +observation, then `registerDiscoveryAndCrudEndpoints` (and the flag) are deleted +and this plugin becomes a pure transport adapter (ADR-0076 D11). diff --git a/packages/plugins/plugin-hono-server/src/hono-anonymous-deny.test.ts b/packages/plugins/plugin-hono-server/src/hono-anonymous-deny.test.ts index 8515d1964a..a32efcafca 100644 --- a/packages/plugins/plugin-hono-server/src/hono-anonymous-deny.test.ts +++ b/packages/plugins/plugin-hono-server/src/hono-anonymous-deny.test.ts @@ -20,7 +20,13 @@ function bootStandardEndpoints(opts: { restConfig?: { api?: { requireAuth?: boolean } }; services: Record; }) { - const plugin = new HonoServerPlugin({ port: 0, restConfig: opts.restConfig as any }); + // Explicit opt-in: the raw surface whose #2567 gate is under test defaults + // OFF now (#4073). + const plugin = new HonoServerPlugin({ + port: 0, + registerStandardEndpoints: true, + restConfig: opts.restConfig as any, + }); const ctx: any = { logger: { info() {}, debug() {}, warn() {}, error() {} }, getKernel: () => ({ getService: (n: string) => opts.services[n] }), diff --git a/packages/plugins/plugin-hono-server/src/hono-discovery.test.ts b/packages/plugins/plugin-hono-server/src/hono-discovery.test.ts index fd94d3e11a..ed427192e1 100644 --- a/packages/plugins/plugin-hono-server/src/hono-discovery.test.ts +++ b/packages/plugins/plugin-hono-server/src/hono-discovery.test.ts @@ -25,7 +25,8 @@ const RUNTIME_DISPATCHER_PLUGIN = 'com.objectstack.runtime.dispatcher'; * which is how this surface decides whether a real discovery owner is present. */ function bootStandardEndpoints(installedPlugins: string[] = []) { - const plugin = new HonoServerPlugin({ port: 0 }); + // Explicit opt-in: the legacy surface under test defaults OFF now (#4073). + const plugin = new HonoServerPlugin({ port: 0, registerStandardEndpoints: true }); const ctx: any = { logger: { info() {}, debug() {}, warn() {}, error() {} }, getKernel: () => ({ diff --git a/packages/plugins/plugin-hono-server/src/hono-plugin.ts b/packages/plugins/plugin-hono-server/src/hono-plugin.ts index b1ba13038e..718af23a43 100644 --- a/packages/plugins/plugin-hono-server/src/hono-plugin.ts +++ b/packages/plugins/plugin-hono-server/src/hono-plugin.ts @@ -52,17 +52,32 @@ export interface HonoPluginOptions { * raw `POST/GET /api/v1/data/:object` (create + read only) and * `GET /api/v1/discovery` / `/.well-known/objectstack`. * - * Every one of these is DUPLICATE supply. `@objectstack/rest` serves full - * `/data` CRUD and, registering first, is what actually answers; the - * dispatcher and REST own discovery and this surface cedes it to them when - * either is present (#4018). The flag exists for a bare host that mounts - * neither. + * @deprecated On its way out (#4073) — opt in only during the deprecation + * window; the surface (and this flag) will be deleted after a release of + * observation, leaving this plugin a pure transport adapter (ADR-0076 D11). + * + * Every path it mounts is DUPLICATE supply, and lesser supply at that: + * C+R only, a subset of the gates, and a discovery payload that predates + * `DiscoverySchema`. `@objectstack/rest` serves full `/data` CRUD behind + * the whole gate stack, REST/the dispatcher own discovery (this surface + * cedes it to them when either is present, #4018), and a composed host + * answers byte-identically with this flag on or off (#4260). It exists + * only for a bare host that mounts neither — and the tax has been real: + * every platform invariant needed re-implementing here after the fact + * (#2567, #3298, #4018). + * + * The default is now `false`. A bare host that relied on it should mount + * `createRestApiPlugin` (`@objectstack/rest`) — it needs the same + * `objectql` this surface already required, and returns full CRUD plus + * the gates — or pass `true` explicitly until the deletion lands. A boot + * with no data/discovery provider at all logs a pointer instead of + * silently 404ing. * * It does NOT gate the current-user endpoints (`/auth/me/permissions`, * `/auth/me/localization`, `/me/apps`) — this plugin is their only provider * anywhere, so they register unconditionally (#4073). * - * @default true + * @default false */ registerStandardEndpoints?: boolean; /** @@ -263,7 +278,9 @@ export class HonoServerPlugin implements Plugin { constructor(options: HonoPluginOptions = {}) { this.options = { port: 3000, - registerStandardEndpoints: true, + // OFF by default (#4073): the convenience surface is deprecated + // duplicate supply. See the option's JSDoc for the migration path. + registerStandardEndpoints: false, useApiRegistry: true, spaFallback: false, ...options @@ -620,6 +637,31 @@ export class HonoServerPlugin implements Plugin { ctx.hook('kernel:ready', async () => { this.registerDiscoveryAndCrudEndpoints(ctx); }); + } else { + // The default is OFF (#4073). For a composed host that is a no-op — + // REST/the dispatcher answer these routes byte-identically either + // way (#4260). The one composition it changes is a BARE host that + // mounts none of the three: it used to inherit the convenience + // surface implicitly and now gets 404s. Say so once at boot, with + // the remedy — the same honesty rule as #4018's discovery cede: + // absence must be loud, not something to diagnose from a silent + // 404. Checked on kernel:ready so every `kernel.use()` has landed, + // and quiet whenever a real API owner is mounted so transport-only + // compositions are not nagged. + ctx.hook('kernel:ready', async () => { + const kernel = ctx.getKernel() as { hasPlugin?(name: string): boolean } | undefined; + const hasPlugin = (name: string) => + typeof kernel?.hasPlugin === 'function' && kernel.hasPlugin(name); + if (!hasPlugin(REST_API_PLUGIN) && !hasPlugin(RUNTIME_DISPATCHER_PLUGIN)) { + ctx.logger.warn( + 'No data/discovery API is mounted on this server: `registerStandardEndpoints` ' + + 'defaults to false (#4073; the convenience surface is deprecated). Mount ' + + '`createRestApiPlugin` from @objectstack/rest (full CRUD + gates) or the ' + + 'runtime dispatcher — or pass `registerStandardEndpoints: true` to keep the ' + + 'legacy surface during the deprecation window.', + ); + } + }); } // Open the listening socket on kernel:listening — this fires diff --git a/packages/plugins/plugin-hono-server/src/hono-standard-endpoints-default.test.ts b/packages/plugins/plugin-hono-server/src/hono-standard-endpoints-default.test.ts new file mode 100644 index 0000000000..521e270e4c --- /dev/null +++ b/packages/plugins/plugin-hono-server/src/hono-standard-endpoints-default.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #4073 — the DEFAULT of `registerStandardEndpoints` is `false`. + * + * The sibling suite (`hono-current-user-endpoints.test.ts`) pins the flag's two + * EXPLICIT positions. This one pins the position nobody writes down: a host + * that does not pass the option at all. Defaults are invisible at call sites — + * this issue's first correction was literally "I checked who passes the option, + * not who relies on the default" — so the default's behavior gets its own pins + * rather than riding on the explicit-`false` ones and hoping the constructor + * spread never drifts. + * + * Also pinned: the boot pointer. The flip is a no-op for a composed host + * (#4260: byte-identical answers with REST/the dispatcher mounted either way); + * the one composition it changes is a BARE host that mounted none of the three, + * which used to inherit `/data` + `/discovery` implicitly and now 404s. That + * absence must be LOUD — a warn naming the flag and the remedy — because a + * silent 404 is exactly the shape of failure this session of work kept paying + * for (`--with-ui` reusing a stale dist, CI green while `objectstack dev` sat + * dead). And it must stay QUIET when a real API owner is mounted, so + * transport-only compositions are not nagged into cargo-culting the flag back. + */ + +import { describe, it, expect } from 'vitest'; +import { HonoServerPlugin } from './hono-plugin'; + +const SURFACE_ROUTES = ['/api/v1/data/:object', '/api/v1/discovery', '/.well-known/objectstack']; +const ME_ROUTES = [ + '/api/v1/auth/me/permissions', + '/api/v1/auth/me/localization', + '/api/v1/me/apps', +]; + +/** + * Boot through the real `init()`/`start()` and fire the registered + * `kernel:ready` hooks — mirrors the sibling suite, plus a warn recorder and a + * configurable `hasPlugin` so the composed/bare distinction is testable. + */ +async function boot(opts: { + pluginOptions?: ConstructorParameters[0]; + installedPlugins?: string[]; +}) { + const plugin = new HonoServerPlugin({ port: 0, cors: false, ...opts.pluginOptions }); + const warns: string[] = []; + const readyHooks: Array<() => unknown> = []; + const ctx: any = { + logger: { + info() {}, debug() {}, error() {}, + warn(msg: string) { warns.push(String(msg)); }, + }, + getKernel: () => ({ + hasPlugin: (name: string) => (opts.installedPlugins ?? []).includes(name), + getService: () => undefined, + }), + registerService: () => {}, + hook: (event: string, fn: () => unknown) => { + if (event === 'kernel:ready') readyHooks.push(fn); + }, + getService: () => undefined, + }; + + await plugin.init(ctx); + await plugin.start(ctx); + for (const fn of readyHooks) await fn(); + + return { app: (plugin as any).server.getRawApp(), warns }; +} + +const paths = (app: any): string[] => (app.routes ?? []).map((r: any) => r.path); + +describe('registerStandardEndpoints defaults to OFF (#4073)', () => { + it('a host that does not pass the option gets no convenience surface — and keeps /me/*', async () => { + const { app } = await boot({}); + const registered = paths(app); + for (const route of SURFACE_ROUTES) { + expect(registered, `${route} must NOT mount by default — the surface is opt-in now`) + .not.toContain(route); + } + // The flip must not take the unconditional endpoints with it. + for (const route of ME_ROUTES) expect(registered).toContain(route); + }); + + it('explicit opt-in still mounts the legacy surface during the deprecation window', async () => { + const { app } = await boot({ pluginOptions: { registerStandardEndpoints: true } }); + const registered = paths(app); + for (const route of SURFACE_ROUTES) expect(registered).toContain(route); + }); + + it('a BARE boot says so loudly: one warn naming the flag and the remedy', async () => { + const { warns } = await boot({ installedPlugins: [] }); + const pointer = warns.filter((w) => w.includes('registerStandardEndpoints')); + expect(pointer, 'a bare host must be told why /data and /discovery are absent').toHaveLength(1); + // The message must carry the remedy, not just the diagnosis. + expect(pointer[0]).toContain('@objectstack/rest'); + expect(pointer[0]).toContain('#4073'); + }); + + it('a COMPOSED boot stays quiet — REST owns the routes, nothing is missing', async () => { + const { warns } = await boot({ installedPlugins: ['com.objectstack.rest.api'] }); + expect(warns.filter((w) => w.includes('registerStandardEndpoints'))).toHaveLength(0); + }); + + it('a dispatcher-composed boot stays quiet too', async () => { + const { warns } = await boot({ installedPlugins: ['com.objectstack.runtime.dispatcher'] }); + expect(warns.filter((w) => w.includes('registerStandardEndpoints'))).toHaveLength(0); + }); + + it('opting in silences the pointer — the legacy surface IS the data API then', async () => { + const { warns } = await boot({ + pluginOptions: { registerStandardEndpoints: true }, + installedPlugins: [], + }); + expect(warns.filter((w) => w.includes('registerStandardEndpoints'))).toHaveLength(0); + }); +}); From 8aca8b50c8286d34bae4aad3a76a42d028dfda4d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 02:25:46 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(authentication):=20the=20minimal=20aut?= =?UTF-8?q?h=20stack=20mounts=20createRestApiPlugin=20=E2=80=94=20its=20/d?= =?UTF-8?q?ata=20example=20needs=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs-drift check flagged six docs; five carry no drift (the quickstart runs `objectstack dev`, a composed stack; plugins/index already describes the hono plugin as transport; implementation-status already attributes /data to @objectstack/rest). This one was real, and it is exactly the bare-host shape the default flip changes: the setup example composes ObjectQL + Driver + Hono + Auth with no REST, and a later snippet on the same page fetches `/api/v1/data/task` — which under the new default 404s on that composition. Fixed the way the changeset's migration path prescribes: mount `createRestApiPlugin` in the example (full CRUD behind the gate stack, same `objectql` dependency), NOT `registerStandardEndpoints: true` — docs should not teach new readers a deprecated flag. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd --- content/docs/permissions/authentication.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/content/docs/permissions/authentication.mdx b/content/docs/permissions/authentication.mdx index 58f95f3d36..223ffb3430 100644 --- a/content/docs/permissions/authentication.mdx +++ b/content/docs/permissions/authentication.mdx @@ -128,6 +128,7 @@ import { DriverPlugin } from '@objectstack/runtime'; import { InMemoryDriver } from '@objectstack/driver-memory'; import { AuthPlugin } from '@objectstack/plugin-auth'; import { HonoServerPlugin } from '@objectstack/plugin-hono-server'; +import { createRestApiPlugin } from '@objectstack/rest'; const kernel = new ObjectKernel(); @@ -141,6 +142,13 @@ await kernel.use(new HonoServerPlugin({ port: 3000, })); +// Data API — serves /api/v1/data/:object with full CRUD behind the gate +// stack. Without it this minimal stack has no data routes: the HTTP server +// plugin is a transport adapter and its legacy built-in data surface is +// deprecated and off by default (#4073). The authenticated fetch further +// down this page reads /api/v1/data/task through this plugin. +await kernel.use(createRestApiPlugin()); + // Authentication plugin await kernel.use(new AuthPlugin({ secret: process.env.OS_AUTH_SECRET,