diff --git a/.changeset/http-server-config-retired.md b/.changeset/http-server-config-retired.md new file mode 100644 index 0000000000..4bbc9095e0 --- /dev/null +++ b/.changeset/http-server-config-retired.md @@ -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` | +| `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. diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx index 708992f028..1673d11013 100644 --- a/content/docs/getting-started/quick-reference.mdx +++ b/content/docs/getting-started/quick-reference.mdx @@ -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 | diff --git a/content/docs/references/system/http-server.mdx b/content/docs/references/system/http-server.mdx index 49616bb6c2..6b3e15e956 100644 --- a/content/docs/references/system/http-server.mdx +++ b/content/docs/references/system/http-server.mdx @@ -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: @@ -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 diff --git a/content/docs/references/system/stack-server.mdx b/content/docs/references/system/stack-server.mdx index 7b9687a695..2d8582e960 100644 --- a/content/docs/references/system/stack-server.mdx +++ b/content/docs/references/system/stack-server.mdx @@ -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 @@ -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 diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index dd04185294..72f2ba3c47 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -280,4 +280,4 @@ directory rather than per file. | `kernel/` | 319 | | `qa/` | 6 | | `shared/` | 25 | -| `system/` | 368 | +| `system/` | 366 | diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index e083eb3fb8..75e3d8ca7c 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -944,9 +944,6 @@ "HistogramBucketConfigSchema (const)", "HttpDestinationConfig (type)", "HttpDestinationConfigSchema (const)", - "HttpServerConfig (type)", - "HttpServerConfigInput (type)", - "HttpServerConfigSchema (const)", "ISettingsCapability (interface)", "ISettingsClient (interface)", "Incident (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index bb9f3c6626..cfde5a17ef 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -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", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 122ce232bc..3a36673ca7 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1257,7 +1257,6 @@ "system/GCounter", "system/HistogramBucketConfig", "system/HttpDestinationConfig", - "system/HttpServerConfig", "system/Incident", "system/IncidentCategory", "system/IncidentNotificationMatrix", diff --git a/packages/spec/src/shared/http.zod.ts b/packages/spec/src/shared/http.zod.ts index 4a51f5740f..8c64374c10 100644 --- a/packages/spec/src/shared/http.zod.ts +++ b/packages/spec/src/shared/http.zod.ts @@ -63,8 +63,13 @@ export type HttpRequest = z.infer; * * 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, @@ -115,8 +120,13 @@ export type CorsConfig = z.infer; * * 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, @@ -152,9 +162,12 @@ export type RateLimitConfig = z.infer; * 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", diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index a65cb33b84..aad58042bb 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -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)'), diff --git a/packages/spec/src/system/http-server.test.ts b/packages/spec/src/system/http-server.test.ts index 678fb2fdc7..ca71b8454e 100644 --- a/packages/spec/src/system/http-server.test.ts +++ b/packages/spec/src/system/http-server.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; +import * as httpServer from './http-server.zod'; import { - HttpServerConfigSchema, RouteHandlerMetadataSchema, MiddlewareType, MiddlewareConfigSchema, @@ -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 }).shape; + expect(Object.keys(routerShape)).toEqual(expect.arrayContaining(['cors', 'staticMounts'])); + const endpointShape = (ApiEndpointSchema as never as { shape: Record }).shape; + expect(Object.keys(endpointShape)).toContain('rateLimit'); }); }); diff --git a/packages/spec/src/system/http-server.zod.ts b/packages/spec/src/system/http-server.zod.ts index f39d86a410..683b750705 100644 --- a/packages/spec/src/system/http-server.zod.ts +++ b/packages/spec/src/system/http-server.zod.ts @@ -1,14 +1,14 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { HttpMethod, CorsConfigSchema, RateLimitConfigSchema, StaticMountSchema } from '../shared/http.zod'; +import { HttpMethod } from '../shared/http.zod'; +import { lazySchema } from '../shared/lazy-schema'; /** * 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: * - Kubernetes: Service and Ingress resources * - AWS: API Gateway configuration @@ -16,74 +16,58 @@ import { HttpMethod, CorsConfigSchema, RateLimitConfigSchema, StaticMountSchema */ // ========================================== -// Server Configuration +// Server Configuration — RETIRED // ========================================== -/** - * HTTP Server Configuration Schema - * Core configuration for HTTP server instances - * - * @example - * { - * "port": 3000, - * "host": "0.0.0.0", - * "cors": { - * "enabled": true, - * "origins": ["http://localhost:3000"] - * }, - * "compression": true, - * "requestTimeout": 30000 - * } - */ -import { lazySchema } from '../shared/lazy-schema'; -export const HttpServerConfigSchema = lazySchema(() => z.object({ - /** - * Server port number - */ - port: z.number().int().min(1).max(65535).default(3000).describe('Port number to listen on'), - - /** - * Server host address - */ - host: z.string().default('0.0.0.0').describe('Host address to bind to'), - - /** - * CORS configuration - */ - cors: CorsConfigSchema.optional().describe('CORS configuration'), - - /** - * Request handling options - */ - requestTimeout: z.number().int().default(30000).describe('Request timeout in milliseconds'), - bodyLimit: z.string().default('10mb').describe('Maximum request body size'), - - /** - * Compression settings - */ - compression: z.boolean().default(true).describe('Enable response compression'), - - /** - * Security headers - */ - security: z.object({ - helmet: z.boolean().default(true).describe('Enable security headers via helmet'), - rateLimit: RateLimitConfigSchema.optional().describe('Global rate limiting configuration'), - }).optional().describe('Security configuration'), - - /** - * Static file serving - */ - static: z.array(StaticMountSchema).optional().describe('Static file serving configuration'), - - /** - * Trust proxy settings - */ - trustProxy: z.boolean().default(false).describe('Trust X-Forwarded-* headers'), -})); - -export type HttpServerConfig = z.infer; -export type HttpServerConfigInput = z.input; +// `HttpServerConfigSchema` / `HttpServerConfig` / `HttpServerConfigInput` and +// the `HttpServerConfig.create()` helper were REMOVED per ADR-0049 +// enforce-or-remove (#4938). The shape declared nine keys — `port`, `host`, +// `cors`, `requestTimeout`, `bodyLimit`, `compression`, `security`, `static`, +// `trustProxy` — and it was doubly inert: +// +// 1. 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. +// 2. ZERO authoring entry — the condition that made this 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 `authorable-surface.json` +// listed and `content/docs/references/` rendered could not even be +// WRITTEN DOWN, let alone take effect. +// +// What actually decides these things, and where to configure each: +// +// | retired key | the live mechanism | +// |---|---| +// | `port` / `host` | the deployment, not the stack — `objectstack serve -p ` / `PORT` | +// | `static` | the transport plugin's `staticMounts` | +// | `cors` | the transport adapter — `OS_CORS_ORIGIN` / `OS_CORS_CREDENTIALS` / `OS_CORS_MAX_AGE` | +// | `security.helmet` | the dispatcher plugin's `securityHeaders` (on by default) | +// | `security.rateLimit` | `defineStack({ server: { security: { rateLimit } } })` — LIVE since #5006 | +// | `trustProxy` | `defineStack({ server: { trustProxy } })` — LIVE since #5006 | +// | `requestTimeout` / `bodyLimit` / `compression` | nothing. No seam 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` (`system/stack-server.zod.ts`), which is the one +// authoring surface for server-level configuration and grows one key at a time, +// each arriving with its consumer. That schema is `strictObject`, so the other +// seven keys are rejected BY NAME there with a per-key prescription — which is +// why this removal needs no `retiredKey()` tombstone: a tombstone is a message +// to whoever writes the key, and the only place anyone can write it already +// answers. Route 3 of the retirement playbook ("nothing parses it → neither"), +// the same shape #4834 / PR #4878 used for the kernel plugin-runtime family. +// +// `cors` is the one key with real business pull (`example-embed-objectql` +// proves embedding is a live scenario), and the 2026-08-04 ruling registered it +// as the FIRST per-key admission candidate into `server:` — to be admitted the +// #4910 way, WITH its executor, when the embedding work is scheduled. It is +// deliberately not parked on the export surface as a dead key in the meantime. +// +// `CorsConfigSchema`, `RateLimitConfigSchema` and `StaticMountSchema` stay in +// `shared/http.zod.ts`: each has other live consumers (`api/router.zod.ts`, +// `api/endpoint.zod.ts`, `system/stack-server.zod.ts`). // ========================================== // Route Registration @@ -352,12 +336,9 @@ export type ServerStatus = z.infer; // Helper Functions // ========================================== -/** - * Helper to create HTTP server configuration - */ -export const HttpServerConfig = Object.assign(HttpServerConfigSchema, { - create: >(config: T) => config, -}); +// The `HttpServerConfig` helper (`Object.assign(HttpServerConfigSchema, { +// create })`) went with the schema it wrapped — see the retirement note at the +// top of this file (#4938). /** * Helper to create middleware configuration diff --git a/packages/spec/src/system/stack-server.test.ts b/packages/spec/src/system/stack-server.test.ts index 86f78c5105..974b821abd 100644 --- a/packages/spec/src/system/stack-server.test.ts +++ b/packages/spec/src/system/stack-server.test.ts @@ -81,6 +81,12 @@ describe('server: carries only keys with a consumer (#4938 stays shut)', () => { const message = result.error!.issues.map((i) => i.message).join('\n'); expect(message).toMatch(/Unrecognized key/); expect(message).toMatch(expected); + // Since #4938 these seven are not merely "not mounted here" — the shape + // that declared them is GONE, and this strict guidance map is the only + // surface left that answers for them. That is why the retirement needed no + // `retiredKey()` tombstone; if this assertion ever stops holding, the + // prescription has drifted away from the removal it stands in for. + expect(message).toMatch(/retired in v17/); }); }); diff --git a/packages/spec/src/system/stack-server.zod.ts b/packages/spec/src/system/stack-server.zod.ts index 0aefc53452..9623b7c687 100644 --- a/packages/spec/src/system/stack-server.zod.ts +++ b/packages/spec/src/system/stack-server.zod.ts @@ -5,8 +5,8 @@ * * ## Why this is NOT `HttpServerConfigSchema` * - * `system/http-server.zod.ts` declares nine keys (`port`, `host`, `cors`, - * `requestTimeout`, `bodyLimit`, `compression`, `security`, `static`, + * `system/http-server.zod.ts` used to declare nine keys (`port`, `host`, + * `cors`, `requestTimeout`, `bodyLimit`, `compression`, `security`, `static`, * `trustProxy`). #4938 measured them: **none had a runtime reader and none was * reachable from any authoring surface** — `stack.zod.ts` had no `server:` key, * so the whole shape was unwritable as well as unread. Mounting it wholesale @@ -22,9 +22,19 @@ * | `security.rateLimit` | `createDispatcherPlugin` → the inbound token bucket (`@objectstack/runtime` `security/inbound-rate-limit.ts`) — an over-budget caller gets `429` + `Retry-After` | * | `trustProxy` | the same limiter's IP resolution — see below | * - * The other seven `HttpServerConfigSchema` keys stay unreachable, and their - * enforce-or-remove fate is tracked by #4938. Adding one here without an - * executor re-opens the hole this narrowness exists to close. + * The other seven `HttpServerConfigSchema` keys were RETIRED with the shape + * that carried them (#4938, ADR-0049 enforce-or-remove): unreachable *and* + * 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 * @@ -170,17 +180,27 @@ export const StackServerConfigSchema = lazySchema(() => strictObject( guidance: { port: 'Not authorable. The listening port belongs to the deployment, not the stack — pass ' - + '`objectstack serve -p ` or set PORT.', + + '`objectstack serve -p ` or set PORT. (`HttpServerConfig.port` was retired in v17, #4938.)', host: 'Not authorable. The bind address belongs to the deployment, not the stack — pass it to ' - + '`objectstack serve`.', + + '`objectstack serve`. (`HttpServerConfig.host` was retired in v17, #4938.)', cors: 'Not authorable here. CORS is owned by the transport adapter and configured by ' - + 'OS_CORS_ORIGIN / OS_CORS_CREDENTIALS / OS_CORS_MAX_AGE.', - compression: 'Not authorable — no runtime reads it (#4938).', - requestTimeout: 'Not authorable — no runtime reads it (#4938).', - bodyLimit: 'Not authorable — no runtime reads it (#4938).', - static: 'Not authorable. Static mounts are configured on the transport plugin (`staticMounts`).', + + 'OS_CORS_ORIGIN / OS_CORS_CREDENTIALS / OS_CORS_MAX_AGE. (`HttpServerConfig.cors` was retired in ' + + 'v17, #4938; it is the registered first candidate for a `server.cors` key, which will arrive with ' + + 'its executor.)', + compression: + 'Not authorable — nothing reads it. `HttpServerConfig.compression` was retired in v17 (#4938) ' + + 'rather than mounted here; response compression is the transport adapter\'s concern.', + requestTimeout: + 'Not authorable — nothing reads it. `HttpServerConfig.requestTimeout` was retired in v17 (#4938) ' + + 'rather than mounted here.', + bodyLimit: + 'Not authorable — nothing reads it. `HttpServerConfig.bodyLimit` was retired in v17 (#4938) ' + + 'rather than mounted here.', + static: + 'Not authorable. Static mounts are configured on the transport plugin (`staticMounts`). ' + + '(`HttpServerConfig.static` was retired in v17, #4938.)', }, }, {