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
73 changes: 73 additions & 0 deletions .changeset/http-server-config-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
"@objectstack/spec": major
---

refactor(spec)!: retire `HttpServerConfigSchema` — nine documented keys with zero readers AND no way to write them (#4938)

`system/http-server.zod.ts` declared `HttpServerConfigSchema` with nine keys —
`port`, `host`, `cors`, `requestTimeout`, `bodyLimit`, `compression`,
`security`, `static`, `trustProxy`. `authorable-surface.json` listed all nine
and `content/docs/references/` rendered them as protocol documentation. Both
halves of the contract were empty:

- **Zero runtime readers.** No package in any repo (objectstack / cloud /
objectui) ever parsed a document with this schema or read a key off it. The
only non-spec mentions were "Used by:" comments in `shared/http.zod.ts`
pointing back at it.
- **Zero authoring entry** — worse than the ordinary declared-but-unread
defect. `stack.zod.ts` had no `server:` key, `config-schema.json` had no
`HttpServerConfig`, and no settings manifest carried it, so the configuration
the docs promised could not even be written down, let alone take effect.

What actually decides these things is three *other* shapes: the CLI `serve`
arguments, the Hono adapter's `ObjectStackHonoOptions`, and
`DispatcherPluginConfig.securityHeaders`. `HttpServerConfigSchema` was
unacquainted with all three. Per ADR-0049 enforce-or-remove, and the 2026-08-04
ruling on #4938, the unreachable face is removed.

FROM → TO, per retired key:

| removed | what to do instead |
|---|---|
| `HttpServerConfig.port` / `.host` | the deployment owns the socket — `objectstack serve -p <port>` / `PORT` |
| `HttpServerConfig.static` | the transport plugin's `staticMounts` |
| `HttpServerConfig.cors` | the transport adapter — `OS_CORS_ORIGIN` / `OS_CORS_CREDENTIALS` / `OS_CORS_MAX_AGE` |
| `HttpServerConfig.security.helmet` | the dispatcher plugin's `securityHeaders` (on by default) |
| `HttpServerConfig.security.rateLimit` | `defineStack({ server: { security: { rateLimit } } })` — LIVE since #5006 |
| `HttpServerConfig.trustProxy` | `defineStack({ server: { trustProxy } })` — LIVE since #5006 |
| `HttpServerConfig.requestTimeout` / `.bodyLimit` / `.compression` | nothing consumes them; they return with an executor or not at all |

Two of the nine were **activated** rather than lost: #5006 mounted
`security.rateLimit` and `trustProxy` on the deliberately narrow
`StackServerConfigSchema`, which grows one key at a time, each arriving with its
consumer. `cors` is registered as the FIRST per-key admission candidate for that
shape — embedding (`example-embed-objectql`) is a real scenario — and will
arrive the #4910 way, key and executor together, rather than sitting on the
export surface as a dead declaration in the meantime.

The retirement kit:

- **No `retiredKey()` tombstone, deliberately** — route 3 of the retirement
playbook ("nothing parses it"), the shape #4834 / PR #4878 used for the kernel
plugin-runtime family. A tombstone is a message to whoever writes the key, and
the only surface on which anyone can write a server key is
`StackServerConfigSchema`; it is `strictObject` and already rejects all seven
by name. Those prescriptions were refreshed from "not authorable — no runtime
reads it" to name the retirement and its replacement.
- **No ADR-0087 D2 conversion**, for the same reason: there is no author source
to rewrite, because the shape was never reachable from an authoring surface.
The channel for code consumers is `api-surface.json` (which lost all three
`HttpServerConfig*` entries) feeding the release-time `spec-changes.json`
diff, plus this changeset.
- Baselines updated deliberately: `json-schema.manifest.json` (−1, the #2978
ratchet fired first and demanded it), `authorable-surface.json` (−9, allowed
by the #4650 gate's path 3 "def no longer emitted by this build"),
`api-surface.json` (−3). Reference docs and the strictness-ledger counts
regenerated.
- **The container, not the file.** `RouteHandlerMetadata` (consumed by
`packages/rest`) and `MiddlewareType` / `MiddlewareConfig` (consumed by
`packages/runtime`) stay, as do `CorsConfigSchema`, `RateLimitConfigSchema`
and `StaticMountSchema` in `shared/http.zod.ts` — each has live consumers
outside the retired shape, so none of them was orphaned by it.

No runtime behaviour changes — that impossibility is the reason for the removal.
2 changes: 1 addition & 1 deletion content/docs/getting-started/quick-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Runtime environment, logging, jobs, caching, and observability.
| **[Change Management](/docs/references/system/change-management)** | `change-management.zod.ts` | ChangeRequest, RollbackPlan | Change tracking |
| **[Collaboration](/docs/references/system/collaboration)** | `collaboration.zod.ts` | Collaboration | Real-time collab |
| **[Encryption](/docs/references/system/encryption)** | `encryption.zod.ts` | Encryption | Encryption & keys |
| **[HTTP Server](/docs/references/system/http-server)** | `http-server.zod.ts` | HttpServerConfig, MiddlewareConfig | HTTP server config |
| **[HTTP Server](/docs/references/system/http-server)** | `http-server.zod.ts` | RouteHandlerMetadata, MiddlewareConfig | Route + middleware metadata |
| **[Job](/docs/references/system/job)** | `job.zod.ts` | Job, JobSchedule | Background job queue |
| **[Logging](/docs/references/system/logging)** | `logging.zod.ts` | LoggingConfig | Structured logging |
| **[Message Queue](/docs/references/system/message-queue)** | `message-queue.zod.ts` | MessageQueueConfig, TopicConfig | Message queuing |
Expand Down
29 changes: 4 additions & 25 deletions content/docs/references/system/http-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ description: Http Server protocol schemas

HTTP Server Protocol

Defines the runtime HTTP server configuration and capabilities.

Provides abstractions for HTTP server implementations (Express, Fastify, Hono, etc.)
Route-registration metadata, middleware declaration and the server-side lifecycle/status vocabulary for HTTP server implementations (Express, Fastify, Hono, etc.)

Architecture alignment:

Expand All @@ -26,32 +24,13 @@ Architecture alignment:
## TypeScript Usage

```typescript
import { HttpServerConfigSchema, MiddlewareConfigSchema, MiddlewareType, RouteHandlerMetadataSchema, ServerCapabilitiesSchema, ServerEventSchema, ServerEventType, ServerStatusSchema } from '@objectstack/spec/system';
import type { HttpServerConfig, MiddlewareConfig, MiddlewareType, RouteHandlerMetadata, ServerCapabilities, ServerEvent, ServerEventType, ServerStatus } from '@objectstack/spec/system';
import { MiddlewareConfigSchema, MiddlewareType, RouteHandlerMetadataSchema, ServerCapabilitiesSchema, ServerEventSchema, ServerEventType, ServerStatusSchema } from '@objectstack/spec/system';
import type { MiddlewareConfig, MiddlewareType, RouteHandlerMetadata, ServerCapabilities, ServerEvent, ServerEventType, ServerStatus } from '@objectstack/spec/system';

// Validate data
const result = HttpServerConfigSchema.parse(data);
const result = MiddlewareConfigSchema.parse(data);
```

---

## HttpServerConfig

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **port** | `integer` | ✅ | Port number to listen on |
| **host** | `string` | ✅ | Host address to bind to |
| **cors** | `{ enabled: boolean; origins: string \| string[]; methods?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>[]; credentials: boolean; … }` | optional | CORS configuration |
| **requestTimeout** | `integer` | ✅ | Request timeout in milliseconds |
| **bodyLimit** | `string` | ✅ | Maximum request body size |
| **compression** | `boolean` | ✅ | Enable response compression |
| **security** | `{ helmet: boolean; rateLimit?: object }` | optional | Security configuration |
| **static** | `{ path: string; directory: string; cacheControl?: string }[]` | optional | Static file serving configuration |
| **trustProxy** | `boolean` | ✅ | Trust X-Forwarded-* headers |


---

## MiddlewareConfig
Expand Down
28 changes: 23 additions & 5 deletions content/docs/references/system/stack-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ description: Stack Server protocol schemas

## Why this is NOT `HttpServerConfigSchema`

`[system/http-server.zod.ts](/docs/references/system/http-server)` declares nine keys (`port`, `host`, `cors`,
`[system/http-server.zod.ts](/docs/references/system/http-server)` used to declare nine keys (`port`, `host`,

`requestTimeout`, `bodyLimit`, `compression`, `security`, `static`,
`cors`, `requestTimeout`, `bodyLimit`, `compression`, `security`, `static`,

`trustProxy`). #4938 measured them: **none had a runtime reader and none was

Expand All @@ -37,11 +37,29 @@ consumer. Today that is exactly two:

| `trustProxy` | the same limiter's IP resolution — see below |

The other seven `HttpServerConfigSchema` keys stay unreachable, and their
The other seven `HttpServerConfigSchema` keys were RETIRED with the shape

enforce-or-remove fate is tracked by #4938. Adding one here without an
that carried them (#4938, ADR-0049 enforce-or-remove): unreachable *and*

executor re-opens the hole this narrowness exists to close.
unread, they were the cleanest remove candidate in the ledger, and their

prescriptions now live in the `guidance` maps below — the only place an

author can write a server key is also the only place that has to answer for

one. Adding one here without an executor re-opens the hole this narrowness

exists to close.

`cors` is the registered exception-in-waiting: the 2026-08-04 ruling named it

the FIRST per-key admission candidate for this shape, because embedding

(`example-embed-objectql`) is a real scenario with real pull. When that work

is scheduled it arrives the #4910 way — key and executor in one change — not

by un-retiring a declaration.

## What `server:` is NOT for

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,4 @@ directory rather than per file.
| `kernel/` | 319 |
| `qa/` | 6 |
| `shared/` | 25 |
| `system/` | 368 |
| `system/` | 366 |
3 changes: 0 additions & 3 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,9 +944,6 @@
"HistogramBucketConfigSchema (const)",
"HttpDestinationConfig (type)",
"HttpDestinationConfigSchema (const)",
"HttpServerConfig (type)",
"HttpServerConfigInput (type)",
"HttpServerConfigSchema (const)",
"ISettingsCapability (interface)",
"ISettingsClient (interface)",
"Incident (type)",
Expand Down
9 changes: 0 additions & 9 deletions packages/spec/authorable-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -5911,15 +5911,6 @@
"system/HttpDestinationConfig:retry",
"system/HttpDestinationConfig:timeout",
"system/HttpDestinationConfig:url",
"system/HttpServerConfig:bodyLimit",
"system/HttpServerConfig:compression",
"system/HttpServerConfig:cors",
"system/HttpServerConfig:host",
"system/HttpServerConfig:port",
"system/HttpServerConfig:requestTimeout",
"system/HttpServerConfig:security",
"system/HttpServerConfig:static",
"system/HttpServerConfig:trustProxy",
"system/Incident:affectedDataClassifications",
"system/Incident:affectedSystems",
"system/Incident:category",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,6 @@
"system/GCounter",
"system/HistogramBucketConfig",
"system/HttpDestinationConfig",
"system/HttpServerConfig",
"system/Incident",
"system/IncidentCategory",
"system/IncidentNotificationMatrix",
Expand Down
27 changes: 20 additions & 7 deletions packages/spec/src/shared/http.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ export type HttpRequest = z.infer<typeof HttpRequestSchema>;
*
* Used by:
* - api/router.zod.ts (RouterConfigSchema)
* - system/http-server.zod.ts (HttpServerConfigSchema)
*
*
* (`system/http-server.zod.ts` embedded this as `HttpServerConfig.cors` until
* #4938 retired that shape. CORS is owned by the transport adapter and
* configured by OS_CORS_ORIGIN / OS_CORS_CREDENTIALS / OS_CORS_MAX_AGE; this
* schema is the registered first candidate for a future `server.cors` key,
* which arrives WITH its executor or not at all.)
*
* @example
* {
* "enabled": true,
Expand Down Expand Up @@ -115,8 +120,13 @@ export type CorsConfig = z.infer<typeof CorsConfigSchema>;
*
* Used by:
* - api/endpoint.zod.ts (ApiEndpointSchema)
* - system/http-server.zod.ts (HttpServerConfigSchema)
*
* - system/stack-server.zod.ts (ServerRateLimitConfigSchema — this shape reused
* verbatim and closed against unknown keys; the LIVE inbound token bucket)
*
* (`system/http-server.zod.ts` embedded this as `HttpServerConfig.security
* .rateLimit` until #4938 retired that shape; the budget itself was not lost —
* #5006 activated it on the narrow `server:` block.)
*
* @example
* {
* "enabled": true,
Expand Down Expand Up @@ -152,9 +162,12 @@ export type RateLimitConfig = z.infer<typeof RateLimitConfigSchema>;
* Configuration for serving static files
*
* Used by:
* - api/router.zod.ts (RouterConfigSchema)
* - system/http-server.zod.ts (HttpServerConfigSchema)
*
* - api/router.zod.ts (RouterConfigSchema — `staticMounts`)
*
* (`system/http-server.zod.ts` embedded this as `HttpServerConfig.static` until
* #4938 retired that shape. Static mounts are configured on the transport
* plugin's `staticMounts`.)
*
* @example
* {
* "path": "/static",
Expand Down
13 changes: 7 additions & 6 deletions packages/spec/src/stack.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,13 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({
*
* DELIBERATELY NARROW (#4910): it carries only keys an executor consumes —
* today `security.rateLimit` (the inbound token bucket that answers 429) and
* `trustProxy` (how that limiter identifies a caller). It is NOT the nine-key
* `HttpServerConfigSchema`: seven of those keys have no reader and no
* authoring surface, and mounting them here would make dead keys writable
* (their enforce-or-remove fate is #4938). Port/host stay a deployment
* concern owned by `objectstack serve -p`; see the schema file for the
* precedence rule and the rest of the rationale.
* `trustProxy` (how that limiter identifies a caller). It is NOT the former
* nine-key `HttpServerConfigSchema`: seven of those keys had no reader and no
* authoring surface, and mounting them here would have made dead keys
* writable — so they were retired with their container instead (#4938,
* ADR-0049). Port/host stay a deployment concern owned by
* `objectstack serve -p`; see the schema file for the precedence rule, the
* per-key prescriptions and the rest of the rationale.
*/
server: StackServerConfigSchema.optional()
.describe('Server-level runtime config consumed by objectstack serve/dev (inbound rate limit, proxy trust)'),
Expand Down
97 changes: 63 additions & 34 deletions packages/spec/src/system/http-server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import * as httpServer from './http-server.zod';
import {
HttpServerConfigSchema,
RouteHandlerMetadataSchema,
MiddlewareType,
MiddlewareConfigSchema,
Expand All @@ -9,44 +9,73 @@ import {
ServerCapabilitiesSchema,
ServerStatusSchema,
} from './http-server.zod';

describe('HttpServerConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = HttpServerConfigSchema.parse({});

expect(config.port).toBe(3000);
expect(config.host).toBe('0.0.0.0');
expect(config.requestTimeout).toBe(30000);
expect(config.bodyLimit).toBe('10mb');
expect(config.compression).toBe(true);
expect(config.trustProxy).toBe(false);
import * as sharedHttp from '../shared/http.zod';
import { RouterConfigSchema } from '../api/router.zod';
import { ApiEndpointSchema } from '../api/endpoint.zod';

/**
* `HttpServerConfigSchema` was retired in v17 (#4938, ADR-0049
* enforce-or-remove): nine keys, zero runtime readers, and — the condition that
* made it worse than the ordinary declared-but-unread defect — zero authoring
* entry, so the configuration the docs promised could not even be written down.
*
* These are the removal's pin tests. There is deliberately NO `retiredKey()`
* tombstone to assert against: a tombstone is a message to whoever writes the
* key, and the only surface on which anyone can write a server key is
* `StackServerConfigSchema`, which is `strictObject` and already answers for
* all seven by name. Those prescriptions are pinned in `stack-server.test.ts`;
* what is pinned HERE is that the export is gone and that nothing else went
* with it.
*/
describe('HttpServerConfig retirement (#4938)', () => {
// `HttpServerConfigSchema` and `HttpServerConfig` were both runtime VALUES
// (the latter via `Object.assign(HttpServerConfigSchema, { create })`), so a
// runtime `in` check is a real witness for them — reverse-verified by pasting
// the removed limb back, which turns these red plus the barrel assertion
// below. `HttpServerConfigInput` is type-only and cannot be seen from here at
// all: a `@ts-expect-error` pin would be a PHANTOM check, because this
// package's tsconfig excludes `**/*.test.ts` from `tsc --noEmit`. Its witness
// is `api-surface.json`, which lost all three entries in this change and is
// ratcheted by `check:api-surface` against the built `dist/*.d.ts`.
it.each([
'HttpServerConfigSchema',
'HttpServerConfig',
])('no longer exports the value `%s`', (name) => {
expect(name in httpServer).toBe(false);
});

it('should accept full configuration', () => {
const config = HttpServerConfigSchema.parse({
port: 8080,
host: '127.0.0.1',
cors: { enabled: true, origins: ['http://localhost:3000'] },
requestTimeout: 60000,
bodyLimit: '50mb',
compression: false,
security: {
helmet: false,
rateLimit: { windowMs: 60000, maxRequests: 100 },
},
trustProxy: true,
});
it('does not re-export the removed shape from the system barrel either', async () => {
const system = await import('./index');
expect('HttpServerConfigSchema' in system).toBe(false);
expect('HttpServerConfig' in system).toBe(false);
});

expect(config.port).toBe(8080);
expect(config.host).toBe('127.0.0.1');
expect(config.compression).toBe(false);
expect(config.trustProxy).toBe(true);
it('keeps the sibling exports that DO have consumers outside spec', () => {
// The removal is the container, not the file: `RouteHandlerMetadata` is
// consumed by `packages/rest/src/route-manager.ts` and `MiddlewareType` /
// `MiddlewareConfig` by `packages/runtime/src/middleware.ts`. A whole-file
// retirement would have broken both.
for (const name of [
'RouteHandlerMetadataSchema',
'MiddlewareType',
'MiddlewareConfigSchema',
'MiddlewareConfig',
]) {
expect(name in httpServer).toBe(true);
}
});

it('should reject invalid port numbers', () => {
expect(() => HttpServerConfigSchema.parse({ port: 0 })).toThrow();
expect(() => HttpServerConfigSchema.parse({ port: 70000 })).toThrow();
expect(() => HttpServerConfigSchema.parse({ port: -1 })).toThrow();
it('leaves the shared value schemas it embedded in place — they are not orphaned', () => {
// Each has at least one live consumer elsewhere, so none of them became
// dead weight when their only `system/` embed point went away.
expect('CorsConfigSchema' in sharedHttp).toBe(true);
expect('StaticMountSchema' in sharedHttp).toBe(true);
expect('RateLimitConfigSchema' in sharedHttp).toBe(true);

const routerShape = (RouterConfigSchema as never as { shape: Record<string, unknown> }).shape;
expect(Object.keys(routerShape)).toEqual(expect.arrayContaining(['cors', 'staticMounts']));
const endpointShape = (ApiEndpointSchema as never as { shape: Record<string, unknown> }).shape;
expect(Object.keys(endpointShape)).toContain('rateLimit');
});
});

Expand Down
Loading
Loading