diff --git a/.changeset/spec-routes-doc-redirect-http-server.md b/.changeset/spec-routes-doc-redirect-http-server.md new file mode 100644 index 0000000000..7bf280a97c --- /dev/null +++ b/.changeset/spec-routes-doc-redirect-http-server.md @@ -0,0 +1,40 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): stop recommending `contributes.routes` for code-handler endpoints — redirect the author-facing materials to the imperative `http.server` mount (#10726) + +`contributes.routes` (`packages/spec/src/kernel/manifest.zod.ts`) has **zero readers** +monorepo-wide: the only non-test read of `manifest.contributes` anywhere reads `kinds`, not +`routes` (`packages/objectql/src/engine.ts:4499`). An author following the shipped guidance +writes a `contributes.routes` entry, gets a clean parse, and serves nothing — ADR-0049's +silent no-op with a published recommendation attached. + +Per the maintainer ruling (2026-08-22, Option B), the four author-facing materials that +recommended the key are corrected **now**, ahead of and independent of the key's removal: + +- `skills/objectstack-api/SKILL.md` — the `apis:` decision table's second row now names the + imperative `http.server` mount and states explicitly that `contributes.routes` parses and + serves nothing. +- `packages/spec/src/api/dispatcher.zod.ts` — the HttpDispatcher protocol doc no longer + claims it "supports dynamic route registration from plugins via contributes.routes". +- `docs/adr/0088-metadata-kind-admission-and-retirement.md` — the `router` retirement row no + longer credits `contributes.routes` as a delivered form. The `router` KIND's retirement is + unaffected: its delivered forms are the imperative mount and, since #5040, declarative + `apis:`. +- `packages/spec/src/ui/app.zod.ts` — the `App.apis` removal message no longer sends + migrators to `contributes.routes`. + +The replacement recommendation was verified live on `main` before it was written, so this is +not a redirect to a second dead form: `http.server` is registered by +`packages/plugins/plugin-hono-server/src/hono-plugin.ts:271` (`providesServices` at :228) and +mounted by real in-tree consumers — `examples/app-showcase/src/system/server/recalc-endpoint.ts` +resolves it on `kernel:ready` and mounts `POST /api/v1/showcase/recalc`, and +`plugin-approvals`, `plugin-sharing`, `cloud-connection` and the CLI's `serve` all resolve the +same service. + +Schema-only change to prose: no key is added, removed or re-typed here. The +`contributes.routes` tombstone itself is #10724's, which is blocked on the `cloud` census +(#10812) — this changeset carries only the doc corrections the ruling ordered not to wait. +The two `content/docs/references/**` pages are the regenerated projection of the two `.zod.ts` +edits (`pnpm --filter @objectstack/spec gen:docs`), not hand edits. diff --git a/content/docs/references/api/dispatcher.mdx b/content/docs/references/api/dispatcher.mdx index 6e934393dc..89fec1c587 100644 --- a/content/docs/references/api/dispatcher.mdx +++ b/content/docs/references/api/dispatcher.mdx @@ -14,7 +14,11 @@ The dispatcher is the central routing component that: 1. Matches incoming request URLs against registered route prefixes 2. Delegates to the corresponding CoreService implementation 3. Returns 503 Service Unavailable when a service is not registered -4. Supports dynamic route registration from plugins via contributes.routes +4. Serves prefixes registered by the kernel services above. Plugins that need + a code handler mount it imperatively on the `http.server` service (resolve + it from the plugin context on `kernel:ready`), NOT through the manifest's + `contributes.routes` key — nothing reads that key, so an entry there parses + cleanly and serves nothing. Architecture alignment: - Kubernetes: API server aggregation layer diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 9af186aed9..ec41b0b742 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -80,7 +80,7 @@ const result = ActionNavItemSchema.parse(data); | **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE still belongs in a plugin manifest `contributes.routes` entry. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — nothing reads it, so an entry there parses cleanly and serves nothing). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | diff --git a/docs/adr/0088-metadata-kind-admission-and-retirement.md b/docs/adr/0088-metadata-kind-admission-and-retirement.md index a14d7fbcc4..82da97281f 100644 --- a/docs/adr/0088-metadata-kind-admission-and-retirement.md +++ b/docs/adr/0088-metadata-kind-admission-and-retirement.md @@ -37,7 +37,7 @@ Their real, consumed forms already live elsewhere, and all are **code contributi | Retired kind | Delivered form | |---|---| -| `router` | plugin manifest `contributes.routes` (HttpDispatcher prefix routing) + imperative `http.server` mounts. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected (its delivered forms are the two above, both code contributions); the endpoint executor is being built under #5040, after which declarative `apis:` becomes a third, real delivered form. | +| `router` | imperative `http.server` mounts (resolve the service from the plugin context and mount on `kernel:ready`) + declarative `apis:` since #5040. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected; the endpoint executor is being built under #5040, after which declarative `apis:` becomes a real delivered form. ⚠️ **Correction (2026-08-22 ruling):** this row also credited plugin manifest `contributes.routes` (HttpDispatcher prefix routing) as a delivered form. That was never true either — the key has zero readers monorepo-wide, so an entry parses cleanly and serves nothing. The key is being removed and every author-facing material that recommended it is corrected to point at the imperative mount; the delivered forms are the two now named above, and the `router` KIND's retirement is unaffected. | | `function` | `defineStack({ functions })` code values (hook-binder & flow `script` body runners) + plugin `contributes.functions` (QL query functions) | | `service` | the plugin/service registry itself (`registerService`) | diff --git a/packages/spec/src/api/dispatcher.zod.ts b/packages/spec/src/api/dispatcher.zod.ts index ee543e9f81..013c749b14 100644 --- a/packages/spec/src/api/dispatcher.zod.ts +++ b/packages/spec/src/api/dispatcher.zod.ts @@ -13,7 +13,11 @@ import { CoreServiceName, ServiceCriticalitySchema } from '../system/core-servic * 1. Matches incoming request URLs against registered route prefixes * 2. Delegates to the corresponding CoreService implementation * 3. Returns 503 Service Unavailable when a service is not registered - * 4. Supports dynamic route registration from plugins via contributes.routes + * 4. Serves prefixes registered by the kernel services above. Plugins that need + * a code handler mount it imperatively on the `http.server` service (resolve + * it from the plugin context on `kernel:ready`), NOT through the manifest's + * `contributes.routes` key — nothing reads that key, so an entry there parses + * cleanly and serves nothing. * * Architecture alignment: * - Kubernetes: API server aggregation layer diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index 38fb366ca9..90a31eab6a 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -1458,8 +1458,10 @@ export const AppSchema = lazySchema(() => strictObject( 'only thing that opens anonymous access, and ADR-0121 D6 then requires an armed ' + '`rateLimit: { enabled: true, windowMs, maxRequests }`. Read the ' + '`declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a ' + - 'security review, not a rename. A route that genuinely needs handler CODE still ' + - 'belongs in a plugin manifest `contributes.routes` entry. ' + + 'security review, not a rename. A route that genuinely needs handler CODE is mounted ' + + 'imperatively instead: resolve the `http.server` service from your plugin context and ' + + 'register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — ' + + 'nothing reads it, so an entry there parses cleanly and serves nothing). ' + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', ), diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index 39a5f13202..2bab236d88 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -156,12 +156,12 @@ behind `/api/v1/data/{object}`, `flow` to the automation pipeline behind `POST /api/v1/automation/{name}/trigger`. An endpoint is a stable URL plus a policy layer over an existing pipeline, never a second execution dialect. -### Choosing between `apis:` and `contributes.routes` +### Choosing between `apis:` and a code handler | Use | When | |:---|:---| | **`defineStack({ apis })`** | The endpoint is a *projection* of something the platform already executes: query/return records, or trigger a flow. No code, no deploy artifact, publish-gated. **Prefer this.** | -| **`contributes.routes`** (plugin manifest) | The endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. This registers a prefix for a plugin-provided service. | +| **`http.server` mount** (plugin code) | The endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. Mount it on `http.server`; `contributes.routes` parses but serves nothing. | If the logic is "a bit of computation, then a record write", express it as a **flow** and point a `type: 'flow'` endpoint at it — that keeps the URL