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
49 changes: 49 additions & 0 deletions .changeset/manifest-contributes-routes-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
"@objectstack/spec": minor
---

feat(spec): retire `contributes.routes` — the plugin-manifest block's last dead member (#10726, ADR-0049 enforce-or-remove; maintainer-ruled Option B 2026-08-22)

<!-- adr-0087: registered plugin-manifest-contributes-routes-retired -->

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`; the migration prescription is
registered under protocol major 18, where `os migrate meta` users will look).

`contributes.routes` was the one member #10724's nine-member retirement
deliberately excluded: removing it needed a ruling, not a tombstone, because
the key was the only *declared* channel for a real capability (serving a
code-handler endpoint) and four published surfaces — a customer-published
skill among them — taught it as working machinery. The measurement (#10627,
controlled, three repos, cloud leg closed clean by #10812) is that nothing
ever read it: the HttpDispatcher never registered a prefix from the
declaration, so an entry parsed cleanly and served nothing. The maintainer
ruled Option B (remove; redirect the author-facing materials to the
imperative mount). The doc corrections landed first (PR #11327); this change
is the removal half, plus the two remaining teaching sites (#11328): the
worked manifest example in `plugin-rest-api.zod.ts` and the `router`
delivered-form comments in `metadata-plugin.zod.ts`.

**What is refused:** authoring `contributes.routes`. It is a `retiredKey()`
tombstone (neither `ManifestSchema` nor the `contributes` object is
`.strict()`, so a plain deletion would have silently stripped the key), so
authoring it is a `tsc` error and a parse error carrying the prescription.

**FROM → TO:**

- `contributes.routes: [{ prefix, service, methods? }]` → mount the route
imperatively: resolve the `http.server` service from the plugin context and
register the handler on `kernel:ready`; delete the key. A declarative
endpoint over a pipeline the platform already runs (query/return records,
trigger a flow) is `defineStack({ apis })`.

**What stays:** `contributes.kinds`, now the block's sole live member
(engine → `registry.registerKind`). Runtime behaviour is unchanged: nothing
ever read the key, so removing it removes no behaviour; a stored manifest
still carrying one degrades to a single `[metadata_spec_invalid]` log line at
registration rather than a boot failure.

D3 semantic entry `plugin-manifest-contributes-routes-retired`; no D2
conversion, because a package manifest is not a stack collection member
(`PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry) and a conversion
would be a transform with no seam that ever runs.
7 changes: 4 additions & 3 deletions content/docs/references/api/dispatcher.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,10 @@ The dispatcher is the central routing component that:
3. Returns 503 Service Unavailable when a service is not registered
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.
it from the plugin context on `kernel:ready`) — the manifest's
`contributes.routes` key was removed in @objectstack/spec 17 (#10726):
nothing ever read it, and authoring it is now a tsc error and a parse
error carrying that prescription.

Architecture alignment:
- Kubernetes: API server aggregation layer
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/metadata.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,7 +88,7 @@ const result = AppDefinitionResponseSchema.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/<manifest.namespace>/<subpath>` 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. |
| **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/<manifest.namespace>/<subpath>` 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 — removed in @objectstack/spec 17, #10726: nothing ever read it, and authoring it is now rejected with its own prescription). 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. |
Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/api/package-api.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,7 +200,7 @@ Install package request
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand DownExpand Up@@ -359,7 +359,7 @@ Upgrade package request
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand DownExpand Up@@ -443,7 +443,7 @@ Resolve dependencies request
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand Down
50 changes: 20 additions & 30 deletions content/docs/references/api/plugin-rest-api.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,41 +28,31 @@ Architecture Alignment:
- Microsoft Dynamics: Web API with entity operations
- Strapi: Auto-generated REST endpoints from schemas

@example Plugin Manifest
@example Serving routes from a plugin (imperative `http.server` mount)
```typescript
{
"name": "rest_api",
"version": "1.0.0",
"type": "server",
"contributes": {
"routes": [
{
"prefix": "/api/v1/discovery",
"service": "metadata",
"methods": ["getDiscovery"],
"middleware": [
{ "name": "response_envelope", "type": "transformation", "enabled": true }
]
},
{
"prefix": "/api/v1/meta",
"service": "metadata",
"methods": ["getMetaTypes", "getMetaItems", "getMetaItem", "saveMetaItem"],
"middleware": [
{ "name": "auth", "type": "authentication", "enabled": true },
{ "name": "request_validation", "type": "validation", "enabled": true }
]
},
{
"prefix": "/api/v1/data",
"service": "data",
"methods": ["findData", "getData", "createData", "updateData", "deleteData"]
}
]
// Routes are mounted in CODE — resolve the `http.server` service from the
// plugin context and register handlers on `kernel:ready` (the service is
// registered by plugin-hono-server; `examples/app-showcase`'s
// recalc-endpoint is a real consumer of this exact shape). The worked
// manifest example that used to sit here declared `contributes.routes`,
// which was removed in @objectstack/spec 17 (#10726): nothing ever read
// it, so every route it showed parsed cleanly and served nothing.
class RestApiPlugin {
name = 'rest_api';
async init(ctx: PluginContext) {
ctx.hook('kernel:ready', async () => {
const server = await ctx.getService<IHttpServer>('http.server');
server.get('/api/v1/discovery', (req, res) => { ... });
server.post('/api/v1/data/:object', (req, res) => { ... });
});
}
}
```

A declarative endpoint over a pipeline the platform already runs
(query/return records, trigger a flow) is `defineStack({ apis })` instead —
no plugin code at all.

<Callout type="info">
**Source:** `packages/spec/src/api/plugin-rest-api.zod.ts`
</Callout>
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/protocol.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1731,7 +1731,7 @@ Install package request
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/kernel/manifest.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ const result = ManifestSchema.parse(data);
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand DownExpand Up@@ -83,7 +83,7 @@ Structured plugin permission grants (ADR-0025 §3.2)
| **drivers** | `never` | optional | [REMOVED] `manifest.contributes.drivers` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — it never had an effect: a storage driver is wired by registering a kernel SERVICE named `driver.*` (the objectql plugin picks it up and calls `registerDriver`), and its only in-repo author was registered that way, not by this declaration. Delete the key. |
| **fieldTypes** | `never` | optional | [REMOVED] `manifest.contributes.fieldTypes` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — there is no `registerFieldType` seam anywhere: the declaration advertised an extension point the platform does not have, so authoring it configured nothing. Delete the key. The field-type vocabulary is the spec `FieldType` enum; extending it is a spec change, not a manifest declaration. |
| **functions** | `never` | optional | [REMOVED] `manifest.contributes.functions` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — nothing ever read it; ObjectQL functions declared here were never registered. Delete the key. Declare functions on the stack (`defineStack({ functions: […] })`), which the hook binder registers via `engine.registerFunction`. |
| **routes** | `{ prefix: string; service: string; methods?: string[] }[]` | optional | API route contributions to HttpDispatcher |
| **routes** | `never` | optional | [REMOVED] `manifest.contributes.routes` was removed in @objectstack/spec 17 (#10726, ADR-0049 enforce-or-remove) — nothing ever read it: the HttpDispatcher never registered a prefix from the declaration, so an entry here parsed cleanly and served nothing while published material kept recommending it. Delete the key. A route that needs real handler CODE is mounted imperatively: resolve the `http.server` service from the plugin context and register the handler on `kernel:ready`. A declarative endpoint over a pipeline the platform already runs (query/return records, trigger a flow) is `defineStack({ apis })`. |
| **commands** | `never` | optional | [REMOVED] `manifest.contributes.commands` was removed in @objectstack/spec 17 (#10724, ADR-0049 enforce-or-remove) — the CLI never resolved commands from this declaration: commands are auto-discovered through oclif's native plugin system (the plugin package declares an `oclif` section in its own `package.json`; see `cli-extension.zod.ts`), and the `objectstack.config.ts` plugins array no longer determines CLI commands. Delete the key. |

### Nested Shape: `Manifest.data[number]`
Expand Down
Loading
Loading