@orkestrel/scaffold 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +8 -8
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1509 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +311 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +437 -6
  62. package/dist/src/core/index.cjs +44 -22
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +43 -23
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +9 -9
@@ -0,0 +1,752 @@
1
+ # Server
2
+
3
+ > A typed HTTP server for the `@orkestrel` line: a node-bound `Server` lifecycle entity
4
+ > that composes a middleware onion around a consumed `@orkestrel/router` dispatcher,
5
+ > beside the `HTTPError` vocabulary and a shared substrate for cookies, WebCrypto
6
+ > tokens, content negotiation, ETag and Range, security headers, Server-Sent Events,
7
+ > and the body pipeline.
8
+
9
+ The server consumes `@orkestrel/router` — routing, matching, and dispatch are that
10
+ package's, never re-implemented here — mechanism, not product policy. Its node face
11
+ adds the upgrade seam, per-request connection-fact injection, and `discoverPort` over
12
+ `node:http` through that package's adapter helpers. Source:
13
+ [`src/server`](../src/server). Surfaced through the `@orkestrel/server` barrel
14
+ (aliased `@src/server` inside this repo).
15
+
16
+ ## Surface
17
+
18
+ Bring your own `@orkestrel/router` dispatcher, mount middleware, and start:
19
+
20
+ ```ts
21
+ import type { MiddlewareHandler } from '@orkestrel/server'
22
+ import { createServer } from '@orkestrel/server'
23
+ import { createDispatcher } from '@orkestrel/router'
24
+
25
+ interface State {
26
+ readonly requestId: string
27
+ }
28
+
29
+ const dispatcher = createDispatcher<State>()
30
+ dispatcher.add({
31
+ method: 'GET',
32
+ path: '/users/:id',
33
+ handler: (_request, context) =>
34
+ Response.json({ id: context.params.id, requestId: context.state.requestId }),
35
+ })
36
+
37
+ const withRequestId: MiddlewareHandler<State> = async (_request, context, next) => {
38
+ const response = await next()
39
+ response.headers.set('X-Request-ID', context.state.requestId)
40
+ return response
41
+ }
42
+
43
+ const server = createServer<State>({
44
+ dispatcher,
45
+ state: () => ({ requestId: crypto.randomUUID() }),
46
+ middleware: [withRequestId],
47
+ })
48
+ const port = await server.start()
49
+ server.address // { address, family, port } for the bound listener
50
+ await server.stop()
51
+ ```
52
+
53
+ A route handler reads `context.state` exactly as middleware wrote it — the
54
+ composed onion terminates in `dispatcher.handle(request, context.state)`, so
55
+ there is no second plumbing between the middleware seam and the router.
56
+
57
+ Cross-face and substrate usage appear under [Patterns](#patterns).
58
+
59
+ ### Factories
60
+
61
+ | API | Kind | Summary |
62
+ | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `createNegotiator` | function | Creates a `NegotiatorInterface` — the reusable content-negotiation machine over the weighted `Accept` family. |
64
+ | `createServer` | function | Creates a `ServerInterface` — the node face's HTTP server facade over a consumed `@orkestrel/router` dispatcher. |
65
+ | `createStream` | function | Creates a `StreamInterface` — a generic Server-Sent-Events stream whose `response` is a fetch-standard streaming `Response` a route returns. |
66
+
67
+ ### Constants
68
+
69
+ A `Shape` cell holds the constant's declared type.
70
+
71
+ | API | Kind | Shape | Summary |
72
+ | ---------------------------- | ----- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `DEFAULT_DRAIN_MS` | const | `10_000` | Names the default graceful-stop deadline `stop()` gives in-flight requests and claimed upgraded sockets, `10_000` ms. |
74
+ | `DEFAULT_BODY_LIMIT` | const | `1_048_576` | Names the default maximum request body size `readBody` accepts before a 413, `1_048_576` bytes. |
75
+ | `DEFAULT_DECOMPRESSED_LIMIT` | const | `16_777_216` | Names the default maximum decompressed request body size, `16_777_216` bytes — the zip-bomb cap the body pipeline's byte-counting `TransformStream` enforces when it transparently decompresses a `Content-Encoding` request body. |
76
+ | `SSE_HEADERS` | const | `Readonly<Record<string, string>>` | Holds the SSE response headers a `Stream` sets on its response, merged under any caller `headers` so a caller repeating one of these keys replaces its value. |
77
+ | `REQUEST_ID_PATTERN` | const | `Readonly<RegExp>` | Defines the strict charset `isValidRequestId` requires an incoming `X-Request-ID` to match, `^[A-Za-z0-9_-]{1,200}$`. |
78
+ | `COMPRESSIBLE_TYPES` | const | `ReadonlySet<string>` | Holds the bare `Content-Type` values `isCompressibleType` treats as compressible, beyond the `text/*` prefix and structured-suffix (`+json`, `+xml`) rules that helper also applies. |
79
+ | `HTTP_ERROR_BRAND` | const | `symbol` | Names the `Symbol.for`-interned brand `HTTPError` carries, `@orkestrel/server.HTTPError`, so `isHTTPError` recognizes an instance across package copies. A consumer never sets it by hand. |
80
+ | `DEFAULT_ENCODINGS` | const | `readonly Encoding[]` | Lists the default `Encoding` content-codings the substrate offers, in preference order — `gzip` then `deflate`. |
81
+
82
+ ### Helpers
83
+
84
+ | API | Kind | Summary |
85
+ | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `compose` | function | Composes an ordered chain of `MiddlewareHandler` layers around a `terminal` handler into one request handler — the frozen middleware seam. |
87
+ | `wrapMiddleware` | function | Wraps one middleware layer around its downstream handler, enforcing the one-call `next` invariant. |
88
+ | `parseCookies` | function | Parses a raw `Cookie:` request header into a `name → value` lookup. |
89
+ | `isCookieName` | function | Checks whether a string is a valid RFC 6265 cookie name — a non-empty run of cookie-token characters with no surrounding or interior whitespace. |
90
+ | `decodeCookieValue` | function | Decodes a cookie value with `decodeURIComponent`, falling back to the raw text when the value is not valid percent-encoding. |
91
+ | `isCookieAttribute` | function | Checks whether a string is safe to interpolate as a `Set-Cookie` attribute value — the guard `serializeCookie` applies to a `Domain` and a `Path` before it emits them. |
92
+ | `serializeCookie` | function | Serializes a cookie into a `Set-Cookie` header value — `name=value` plus its attributes. |
93
+ | `resolveSecure` | function | Resolves a cookie's effective `Secure` flag from its `CookieOptions` `secure` setting and whether the request arrived over TLS. |
94
+ | `writeSignedCookie` | function | Writes a signed cookie — HMAC-signs `value` with `signToken` and appends it as a `Set-Cookie` (the inverse of `readSignedCookie`). |
95
+ | `readSignedCookie` | function | Reads and verifies a signed cookie off a request — total, returning the embedded value or `undefined` (the inverse of `writeSignedCookie`). |
96
+ | `clearCookie` | function | Clears a cookie — appends a `Set-Cookie` that expires it immediately (`Max-Age=0`). |
97
+ | `signToken` | function | Signs a value into a stateless, HMAC-SHA256 token — `<payload>.<signature>`. |
98
+ | `verifyToken` | function | Verifies a stateless token and returns its embedded value — total, never throws. |
99
+ | `decodeTokenPayload` | function | Decodes and narrows a signed token's base64url JSON payload, honoring its expiry — the shared decode step `verifyToken` applies after a signature match. |
100
+ | `normalizeSecret` | function | Normalizes a `TokenSecret` to a concrete list of usable secrets — the list behind both `signToken` and `verifyToken`. |
101
+ | `parseAcceptHeader` | function | Parses a weighted `Accept` / `Accept-Encoding` / `Accept-Language` header into its q-sorted entries. |
102
+ | `computeCodingQuality` | function | Computes the client's quality (q) for one content-coding from the parsed `Accept-Encoding` entries — the scoring leaf `resolveCoding` runs over each offered coding. |
103
+ | `resolveCoding` | function | Picks the highest-scoring content-coding the server offers against already parsed `Accept-Encoding` entries — the shared selection leaf behind `negotiateEncoding` and a `Negotiator`'s `encoding` axis. |
104
+ | `negotiateEncoding` | function | Selects the best content-coding for a raw `Accept-Encoding` header from the codings the server offers. |
105
+ | `matchMediaType` | function | Reports the rank and quality of one `candidate` media type against the parsed `Accept` entries — the generic media-type primitive the `Negotiator`'s `negotiate` uses to score each `available` candidate. |
106
+ | `computeLanguageQuality` | function | Computes the client's quality for one `candidate` language from the parsed `Accept-Language` entries — the scoring leaf the `Negotiator`'s `language` axis runs over each offered tag. |
107
+ | `isCompressibleType` | function | Checks whether a `Content-Type` is worth compressing. |
108
+ | `computeBodyETag` | function | Computes a content `ETag` over a fully-buffered response body by using WebCrypto. |
109
+ | `unwrapETag` | function | Strips the weak indicator (`W/`) from an entity-tag, returning its opaque comparison body — the reduction `matchesETag` applies to each side before the RFC 7232 §2.3.2 weak comparison. |
110
+ | `matchesETag` | function | Checks whether a request's `If-None-Match` header matches a resource's current `ETag` — the RFC 7232 §2.3.2 weak comparison. |
111
+ | `parseRange` | function | Parses an HTTP `Range` request header against a known resource `size` — total, returning a `RangeSpec` or `undefined`. |
112
+ | `resolveOrigin` | function | Resolves the `Access-Control-Allow-Origin` value for a request. |
113
+ | `mergeVary` | function | Merges a `Vary` value into an existing `Vary` header without duplication. |
114
+ | `resolveSecurityHeader` | function | Resolves one opt-out, value-bearing security header. |
115
+ | `isValidRequestId` | function | Checks whether a client-supplied `X-Request-ID` is safe to echo into a response header and `context.state`. |
116
+ | `computeIPv6Network` | function | Computes the `/64` network of a full IPv6 address, or `undefined` when the input is not a plain IPv6 address to collapse. |
117
+ | `computeClientKey` | function | Collapses a client IP into its rate-limit bucket key — an IPv6 address to its `/64` network, an IPv4 (or IPv4-mapped) address unchanged. |
118
+ | `serializeEvent` | function | Serializes one `SSEMessage` to the SSE wire. |
119
+ | `isDangerousKey` | function | Checks whether a key is a prototype-pollution vector — `__proto__`, `constructor`, or `prototype`, each of which can reach and mutate `Object.prototype` when it is assigned onto a normal object. |
120
+ | `scrubPrototype` | function | Strips the prototype-pollution keys from a parsed value in place, recursively. |
121
+ | `collectRequestBody` | function | Collects a `Request` body into a single `Uint8Array`, enforcing a size limit. |
122
+ | `parseEncoding` | function | Parses a raw `Content-Encoding` header value into a decompressible `Encoding` — the boundary `readBody` coerces through to decide whether a request body needs transparent decompression. |
123
+ | `decompressRequestBody` | function | Decompresses an already-collected, `gzip`/`deflate`-encoded byte sequence transparently through `DecompressionStream`, capping the decompressed output — the zip-bomb defense. |
124
+ | `readBody` | function | Collects and decodes a `Request` body — the shared body-collection pipeline surfaced to middleware and handlers as the middleware context's cached `body()`. An empty body and a malformed `application/json` body each decode to `undefined`. |
125
+ | `isHTTPError` | function | Narrows an unknown caught value to an `HTTPError`, including a subclass such as `ContentTooLargeError`, and recognizes an instance from another copy of this package through a structural brand fallback. |
126
+ | `isServerError` | function | Narrows an unknown caught value to a `ServerError` — the code-bearing refusal of a call the caller programmed. |
127
+ | `isAddressInfo` | function | Checks whether a `node:net` `server.address()` return is the structured `AddressInfo` (carrying a numeric `port`) rather than a pipe `string` or `null` — the total, never-throwing narrow `discoverPort` and the `Server`'s own port resolution read the bound port through. |
128
+ | `probePort` | function | Binds and closes a throwaway TCP server to resolve one available port. |
129
+ | `discoverPort` | function | Finds a free TCP port — binds a throwaway `node:net` server on a `preferred` port where one is given and on an ephemeral port otherwise, reads the bound port, closes the server, and resolves that port. |
130
+
131
+ ### Classes
132
+
133
+ | API | Kind | Summary |
134
+ | ---------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
135
+ | `HTTPError` | class | Represents an error a handler (or middleware) throws to produce an HTTP response of a specific status. |
136
+ | `ContentTooLargeError` | class | Represents the `HTTPError` thrown when a request body exceeds the body pipeline's size limit — a `413 Content Too Large`. |
137
+ | `ServerError` | class | Represents the error this package raises when a caller programmed a call the entity refuses, carrying `'STATUS'` or `'NEXT'` as its code. |
138
+ | `Negotiator` | class | Represents the content-negotiation machine over the weighted `Accept` family — a reusable, cross-middleware entity rather than a middleware. Implements exactly `NegotiatorInterface`. |
139
+ | `Server` | class | Represents the HTTP server facade — an observable `node:http` lifecycle composing this module's own middleware onion around a consumed `@orkestrel/router` dispatcher. Implements exactly `ServerInterface`. |
140
+ | `Stream` | class | Represents the Server-Sent-Events handle over an open, fetch-standard streaming `Response`. Implements exactly `StreamInterface`. |
141
+
142
+ ### Types
143
+
144
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
145
+
146
+ | Type | Kind | Shape | Summary |
147
+ | ------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
148
+ | `MiddlewareContext` | interface | `{ url, method, state } plus body` | Represents the composition context — plain data, one per request, shared by every middleware and, as `state`, by the route handlers behind the dispatcher. |
149
+ | `NextFunction` | type | `(request?: Request) => Promise<Response>` | Represents the downstream continuation a `MiddlewareHandler` invokes to run the rest of the onion — guarded so a second call within one invocation rejects. |
150
+ | `MiddlewareHandler` | type | `(request: Request, context: MiddlewareContext<TState>, next: NextFunction) => Response \| Promise<Response>` | Represents one link in the middleware onion — runs around the rest of the chain. |
151
+ | `Connection` | interface | `{ ip?, encrypted }` | Represents the per-request connection facts the server face injects — the only data that genuinely exists solely on the socket, surfaced so middleware and a consumer's `state` factory stay core-pure. |
152
+ | `TokenSecret` | type | `string \| readonly string[]` | Represents a secret, or a `[current, ...older]` rotation list, for signing and verifying a stateless, HMAC-signed token. |
153
+ | `TokenOptions` | interface | `{ secret, ttl? }` | Options for `signToken` — how a stateless, HMAC-signed token is minted. |
154
+ | `CookieOptions` | interface | `{ path?, domain?, maxAge?, httpOnly?, secure?, sameSite? }` | Represents the `Set-Cookie` attributes for `serializeCookie` (and any signed-cookie transport built over it). |
155
+ | `AcceptEntry` | interface | `{ value, q }` | Represents one parsed entry of a weighted `Accept` / `Accept-Encoding` / `Accept-Language` header — a value and its quality weight, the element type `parseAcceptHeader` returns (sorted by `q` descending). |
156
+ | `MediaMatch` | interface | `{ q, rank }` | Rates one candidate media type against a parsed `Accept` header — the quality and specificity `matchMediaType` reports for the best matching `AcceptEntry`. |
157
+ | `Encoding` | type | `'gzip' \| 'deflate' \| 'identity'` | Represents a content-coding the substrate compresses or decompresses with. Its members are the `Content-Encoding` and `Accept-Encoding` token vocabulary the substrate understands. |
158
+ | `FormatHandlerMap` | type | `Readonly<Record<string, (request: Request, context: MiddlewareContext<TState>) => Response \| Promise<Response>>>` | Represents a map of media type → handler for `NegotiatorInterface.format` — the content-negotiation dispatch table. |
159
+ | `NegotiatorInterface` | interface | `{} plus negotiate, encoding, language, format` | Represents content negotiation over the weighted `Accept` family — a reusable, cross-middleware machine (not itself a middleware). |
160
+ | `SSEMessage` | interface | `{ data, event?, id?, retry? }` | Represents one Server-Sent Event to serialize to the wire. |
161
+ | `StreamOptions` | interface | `{ status?, headers? }` | Options for a `StreamInterface` — how `createStream` opens the streaming response. |
162
+ | `StreamInterface` | interface | `{ response, closed } plus write, comment, drain, end` | Represents a handle to write Server-Sent Events to an open, fetch-standard streaming `Response` — the generic streaming surface `createStream` returns over a `ReadableStream`. |
163
+ | `RangeSpec` | type | `{ readonly satisfiable: true; readonly start: number; readonly end: number } \| { readonly satisfiable: false }` | Represents the parsed outcome of an HTTP `Range` request header. |
164
+ | `BodyOptions` | interface | `{ limit?, decompression? }` | Options for `readBody` — how the shared body-collection pipeline caps and decompresses a request body. |
165
+ | `ServerStatus` | type | `'idle' \| 'starting' \| 'listening' \| 'stopping' \| 'stopped'` | Represents the `Server`'s lifecycle state. |
166
+ | `ServerErrorCode` | type | `'STATUS' \| 'NEXT'` | Represents the machine-readable category a `ServerError` carries — `'STATUS'` for a lifecycle call the current status forbids, `'NEXT'` for a middleware that called its `next` a second time. |
167
+ | `RequestLine` | interface | `{ method, url }` | Identifies the request a server-level fault came from — its method and its parsed URL. |
168
+ | `ResponseRecord` | interface | `{ method, pathname, status, ms }` | Records one finished request — the payload `ServerEventMap.response` carries. |
169
+ | `ServerEventMap` | type | `{ start, request, upgrade, error, stop, drain, response }` | Represents the `Server`'s observable lifecycle events. |
170
+ | `UpgradeHandler` | type | `(request: IncomingMessage, socket: Duplex, head: Buffer) => boolean` | Represents a raw `node:http` protocol-upgrade claimant — registered through `ServerInterface.upgrade`. |
171
+ | `ConnectionStateFunction` | type | `(connection: Connection) => TState` | Derives a consumer's per-request `TState` from the adapter-injected `Connection` — `ServerOptions.state`, invoked once per request before the middleware onion runs. |
172
+ | `ServerOptions` | interface | `{ dispatcher, state, middleware?, host?, port?, drain?, limit?, expose?, report?, timeouts?, sockets?, on?, error? }` | Options for `createServer` — the dispatcher and per-request state factory the server requires, plus its listener, drain, boundary, timeout, socket-cap, and emitter knobs. |
173
+ | `ServerInterface` | interface | `{ id, status, port, address, dispatcher, emitter } plus use, upgrade, start, stop, destroy` | Represents the HTTP server facade — an observable `node:http` lifecycle that composes a middleware onion (this module's own middleware seam) around a consumed `@orkestrel/router` `DispatcherInterface`. |
174
+
175
+ Each interface's `readonly` data members stay Surface rows, and its call-signature
176
+ members are documented under [Methods](#methods). `ServerInterface.address` is the
177
+ bound node `AddressInfo` while the listener is active and `undefined` otherwise.
178
+
179
+ ## Methods
180
+
181
+ The public methods of `NegotiatorInterface`, `StreamInterface`, and
182
+ `ServerInterface` — every call-signature member listed (their `readonly` data
183
+ members stay Surface rows). `Negotiator`, `Server`, and `Stream` implement
184
+ their interfaces exactly, so their tables also double as each class's
185
+ instance-method surface.
186
+
187
+ #### `NegotiatorInterface`
188
+
189
+ `negotiate` is the generic media-type primitive; `encoding` / `language` are
190
+ its sibling axes over the same q-value parser; `format` is the dispatcher —
191
+ it reads the request `Accept`, negotiates a `FormatHandlerMap`'s keys, and
192
+ invokes the winner, or answers `406`.
193
+
194
+ The axes diverge on an absent, empty, or unparseable header. `encoding`
195
+ resolves to `undefined` — no compression — rather than to the first offered
196
+ coding, because an absent `Accept-Encoding` makes identity the correct answer;
197
+ `negotiate` and `language` fall back to the first offered value instead.
198
+
199
+ | Method | Returns | Summary |
200
+ | ----------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
201
+ | `negotiate` | `string \| undefined` | Picks the best `available` value for a weighted `Accept`-style `header` — the generic media-type primitive (`encoding` / `language` build on it). |
202
+ | `encoding` | `Encoding \| undefined` | Picks the best `available` content-coding for an `Accept-Encoding` header — the coding axis of the same q-value parser (a bare `*` wildcard ⇒ the first `available`). |
203
+ | `language` | `string \| undefined` | Picks the best `available` language for an `Accept-Language` header — `negotiate` with a language-prefix match (`en` accepts `en-US`) and a bare `*` wildcard. |
204
+ | `format` | `Promise<Response>` | Dispatches to the handler whose media type the client most prefers — reads the request `Accept`, negotiates against `handlers`' keys, and invokes the winner; `406` when none is acceptable. |
205
+
206
+ #### `StreamInterface`
207
+
208
+ `write` always accepts an event while open and returns whether the local
209
+ `ReadableStream` queue still has positive desired size afterward. A producer
210
+ receiving `false` parks on `drain` before writing again. This is process-local
211
+ queue state, not proof that the remote peer consumed bytes; the router's
212
+ drain-honoring response pump makes socket pressure stop pulls so the local
213
+ queue can faithfully signal that transport pressure. Ignoring the boolean
214
+ preserves the prior unconditional-enqueue behavior. The default queue strategy
215
+ counts chunks rather than their byte lengths, so a producer needing a byte
216
+ bound must also cap each individual event. The route must return `response`
217
+ before its producer awaits a `false` write, because no consumer can pull the
218
+ body before receiving that response.
219
+
220
+ | Method | Returns | Summary |
221
+ | --------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `write` | `boolean` | Serializes and enqueues one `SSEMessage` to the wire, reporting whether the process-local queue still has capacity afterward — `false` also once the stream is closed. |
223
+ | `comment` | `void` | Writes a `: text` SSE comment line — a keep-alive a conforming parser ignores, and a no-op once the stream is closed. |
224
+ | `drain` | `Promise<void>` | Parks until the process-local stream queue has capacity again — resolving on the consumer pull that restores it, or immediately when capacity is already available or the stream is closed, and never polling. |
225
+ | `end` | `void` | Ends the stream, completing the response — a no-op once already `closed`, and it settles any parked producer. |
226
+
227
+ #### `ServerInterface`
228
+
229
+ `use` mounts middleware (one handler or an array); `upgrade`
230
+ registers a raw protocol-upgrade claimant; `start` binds the listener and
231
+ resolves the actually-bound port while accepting an optional caller
232
+ `AbortSignal`; `stop` gracefully drains then closes; `destroy` is the
233
+ terminal, idempotent teardown. `stop` and `destroy` always resolve: an upgraded socket a
234
+ handler claimed is drained up to the `drain` deadline and then destroyed,
235
+ never waited on forever.
236
+
237
+ | Method | Returns | Summary |
238
+ | --------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
239
+ | `use` | `void` | Appends one middleware, or an array of them in order, to the onion, outer-to-inner in call order. |
240
+ | `upgrade` | `void` | Registers an `UpgradeHandler` claimant that runs in registration order; a claimed socket is tracked until it closes. |
241
+ | `start` | `Promise<number>` | Binds the configured `host` and `port`, or an ephemeral port, under an optional caller `AbortSignal`, and resolves the actually-bound port. |
242
+ | `stop` | `Promise<void>` | Stops gracefully: refuses new connections, fires the stop signal, drains in-flight requests and claimed upgraded sockets up to the `drain` deadline, then closes. |
243
+ | `destroy` | `Promise<void>` | Tears down for good: force-closes the listener and every socket, then the emitter — terminal and idempotent from any state. |
244
+
245
+ ## Contract
246
+
247
+ These invariants hold across `src/server` ↔ `server.md`.
248
+
249
+ 1. **Doc ↔ source bijection.** Every `function` / `class` / `interface` /
250
+ `type` / `const` row in the `## Surface` tables is a real export of its
251
+ source directory, and every export appears as a Surface row — exhaustive,
252
+ both directions.
253
+ 2. **Doc ↔ source method bijection.** The `## Methods` tables list exactly
254
+ `NegotiatorInterface`'s, `StreamInterface`'s, and `ServerInterface`'s public
255
+ methods — exhaustive, both directions — and `Negotiator` / `Stream` /
256
+ `Server` expose the same public methods, no more.
257
+ 3. **Status machine + bound address + restart-fresh-abort.**
258
+ `idle → starting → listening → stopping → stopped`; `start()` from
259
+ `listening`/`starting`/`stopping` rejects with a `ServerError` of code
260
+ `'STATUS'`, carrying that status in its `context` and narrowed by
261
+ `isServerError`; each `start()` mints a fresh stop signal, so a restarted
262
+ server is never born aborted; `address` is the real bound `AddressInfo`
263
+ after a successful start and `undefined` before start and after stop or
264
+ destroy; `stop()`/`destroy()` are idempotent no-ops from a state with
265
+ nothing to tear down; `EADDRINUSE` rejects `start()` outright — no silent
266
+ ephemeral fallback (use `discoverPort` up front for a guaranteed-free
267
+ port).
268
+ 4. **Startup is bounded and caller-cancellable.** `start(signal?)` observes
269
+ caller cancellation only while binding; `timeouts.start` independently
270
+ bounds the bind (`0` permits no startup window). Cancellation or deadline
271
+ expiry closes the partial listener, clears the startup deadline, resets
272
+ the entity to `idle`, and rejects; expiry rejects with a `DOMException`
273
+ named `TimeoutError`, while caller cancellation rejects with that signal's
274
+ `reason`. A later `start()` is therefore permitted. Aborting the caller
275
+ signal after a successful start does not stop a live server.
276
+ 5. **Graceful drain is event-driven, never a busy-loop.** `stop()` fires the
277
+ stop signal, arms a `@orkestrel/timeout` deadline, and parks on the
278
+ drainable count reaching zero or the deadline firing (a wake-park, not
279
+ polling); it then emits `drain` with the still-pending counts and closes —
280
+ dropping idle keep-alive sockets always, force-closing every open socket
281
+ only when the deadline fired with work still pending (or on `destroy()`).
282
+ Drainable work is every in-flight request plus every upgraded socket a
283
+ handler claimed, because a long-lived upgraded connection is work a
284
+ graceful stop lets finish rather than cuts mid-frame. `drain` carries
285
+ both counts, so a caller can tell a clean stop from a forced one. This is
286
+ also what makes `stop()` and `destroy()` always resolve: node detaches an
287
+ upgraded socket from its own connection set, so neither
288
+ `closeIdleConnections()` nor `closeAllConnections()` reaches it while
289
+ `server.close()` still waits on it, and the server therefore tracks each
290
+ claimed socket until it closes and destroys the survivors itself on a
291
+ forced close. The claimant still owns the socket; tracking only watches it.
292
+ A handler that wants a protocol-clean goodbye — a WebSocket close frame —
293
+ sends it on the `stop` event, which fires before the drain begins, and the
294
+ drain then settles on that close instead of running the deadline out. A
295
+ socket nothing closes costs `stop()` the whole `drain` budget and is then
296
+ cut, so lower `drain` for a faster shutdown.
297
+ 6. **The built-in boundary is lifecycle machinery, not policy — one seam
298
+ that spans setup and dispatch.** The `Server` wraps the whole per-request
299
+ lifecycle in an inner phase and an outer phase of the same boundary. The
300
+ inner phase covers only `buildRequest`: a malformed request (for example,
301
+ an unparsable `Host`) answers a plain `400`, with no `error` emit, no
302
+ `report` call, and no `response` emit, because nothing downstream ever ran
303
+ and no parsed `Request` exists yet to derive its facts from. The outer
304
+ phase covers everything after — a throwing `this.#state(connection)`
305
+ through the middleware/dispatcher run — where a thrown `HTTPError` renders
306
+ as its own status + message; any
307
+ other throw renders `500` with its message hidden unless `expose` is set,
308
+ `report` is invoked with the caught error plus the originating request's
309
+ `{ method, url }` (its own throw swallowed so reporting can never crash
310
+ the response), and `error` is emitted with that same `{ method, url }` as
311
+ its second argument. Beneath this single seam sits one server-owned last
312
+ resort: if writing the mapped response itself throws, the connection is
313
+ destroyed rather than left half-written or crashing the process — the
314
+ middleware package may still ship a richer boundary that short-circuits
315
+ earlier. On both the success path and this outer-boundary error path, a
316
+ `response` event fires once the response has been sent, carrying
317
+ `{ method, pathname, status, ms }` — so observability covers every request
318
+ that reached the middleware pipeline, exactly once, regardless of outcome.
319
+ A request rejected at the inner `buildRequest` boundary is the one
320
+ exception: it emits no `response` at all.
321
+ 7. **Upgrade fan-out is isolated, first-claimer-wins.** Registered
322
+ `UpgradeHandler`s run in registration order; the first to return `true`
323
+ claims the socket and stops the fan-out; a handler that throws is treated as
324
+ declined (the throw surfaces on `error` with no request context — `error`'s
325
+ second argument is `undefined` on the upgrade path, since no fetch
326
+ `Request` exists there, only a raw `IncomingMessage` — and never crashes
327
+ the process) and the fan-out continues; an upgrade nothing claims destroys
328
+ the socket so it never leaks a dangling connection. A claimed socket is
329
+ tracked until it closes, which is what item 5's drain and forced close
330
+ act on — ownership stays with the claimant either way.
331
+ 8. **Body read exactly once, capped, zip-bomb-safe, scrubbed.**
332
+ `MiddlewareContext.body()` is lazy and cached, so a body-parsing middleware
333
+ and the eventual handler both reading it consume the underlying stream
334
+ exactly once; `readBody` caps the wire size (`ContentTooLargeError`/413 over
335
+ `limit`), transparently decompresses a `gzip`/`deflate` body through a
336
+ byte-counting `TransformStream` that aborts the instant decompressed output
337
+ would exceed `decompression` (fail before materializing a decompression
338
+ bomb, since `DecompressionStream` has no `maxOutputLength`), and scrubs
339
+ `__proto__`/`constructor`/`prototype` keys from a parsed JSON body
340
+ (`scrubPrototype`) before it is ever handed to application code.
341
+ 9. **Cookie + token jewels preserved.** `parseCookies` rejects a
342
+ whitespace-padded name so a `' __Host-x'` never reconciles into a
343
+ protected `__Host-` name; `serializeCookie` throws on a `Domain`/`Path`
344
+ injection attempt rather than silently dropping it; a `sameSite: 'None'`
345
+ cookie is always `Secure` regardless of the `secure` setting;
346
+ `resolveSecure` derives `Secure` from the connection's TLS fact whenever
347
+ `secure` is left `undefined` (omitted).
348
+ `verifyToken` is total (malformed / tampered / expired / empty-rotation all
349
+ yield `undefined`, never throw); the expiry is HMAC-covered inside the
350
+ signed payload; a `TokenSecret` rotation list signs with the first secret
351
+ and verifies against any of them; comparison is constant-time through
352
+ `crypto.subtle.verify` (the old `safeCompare` is retired, not ported).
353
+ 10. **Seam semantics: returning onion.** Each `MiddlewareHandler` receives a
354
+ `next` that, called, runs the downstream chain and resolves its `Response`;
355
+ not calling it short-circuits with the middleware's own `Response`; a
356
+ second call to the same `next` within one invocation rejects with a
357
+ `ServerError` of code `'NEXT'` (the double-`next` guard) — a middleware can
358
+ transform the request (`next(newRequest)`), transform the response (mutate
359
+ after `await next()`), or short-circuit, but never fork the chain. That
360
+ rejection escapes into the request boundary, which carries no `status` for
361
+ it and answers a generic 500.
362
+ 11. **The bag is the router's state.** `compose`'s `terminal` is
363
+ `(request, context) => dispatcher.handle(request, context.state)` — the
364
+ exact object every middleware wrote into `context.state` is what a route
365
+ handler reads as `RouteContext.state`. No second plumbing.
366
+ 12. **Connection facts are injected once, at the adapter boundary.**
367
+ `Connection` (`ip`, `encrypted`) is built per-request from the raw
368
+ socket and handed to `ServerOptions.state` — `X-Forwarded-For` is never
369
+ implicitly trusted; a deployment behind a trusted proxy derives its own
370
+ client key explicitly in `state` or in middleware.
371
+ 13. **The stop signal is observable inside a handler.** The `Request`'s
372
+ `signal` (already tied to client disconnect by the router's
373
+ `buildRequest`) is linked, through `@orkestrel/abort`'s `linkSignal`, to the
374
+ server's per-run stop signal — so a handler awaiting `request.signal`
375
+ observes either the client disconnecting or the server calling `stop()`,
376
+ closing the old design's latent gap.
377
+ 14. **Enterprise timeout knobs, Slowloris-guarded.** `timeouts.request` /
378
+ `timeouts.headers` / `timeouts.keepalive` map onto `node:http`'s
379
+ `requestTimeout` / `headersTimeout` / `keepAliveTimeout`; construction
380
+ throws a `TypeError` when `headers` exceeds `keepalive` (the Slowloris
381
+ footgun) — a guard at the boundary, never on the hot path.
382
+ 15. **Socket caps map without policy.** `sockets.connections` /
383
+ `sockets.headers` / `sockets.requests` apply directly to node's
384
+ `maxConnections` / `maxHeadersCount` / `maxRequestsPerSocket` before bind.
385
+ Each is optional, so omission preserves node's native default; `0` keeps
386
+ each native meaning (reject all connections for `connections`, unlimited
387
+ for `headers` and `requests`).
388
+ 16. **Content negotiation is total and q-value-linear.** `parseAcceptHeader`
389
+ is a single pass with no backtracking (ReDoS-safe); a `;q=0` entry is kept
390
+ (an explicit rejection a caller must honor, never silently dropped); an
391
+ absent/malformed `Accept` header resolves to the any-range (the first
392
+ offered value/handler) rather than rejecting.
393
+ 17. **`expose: false` leaks nothing; `HTTPError` messages always surface.** A
394
+ generic (non-`HTTPError`) throw's message is hidden behind a fixed
395
+ `'Internal Server Error'` string unless `expose` is explicitly `true`; an
396
+ `HTTPError`'s own `message` is always client-facing (it is the handler's
397
+ deliberate signal), independent of `expose`.
398
+ 18. **`isHTTPError` recognizes an `HTTPError` across package copies, not only
399
+ `instanceof`.** A version-skewed or workspace-linked duplicate install of
400
+ this package produces a second, distinct `HTTPError` constructor —
401
+ `instanceof` fails across the copies even though the thrown value is
402
+ structurally identical, which would otherwise collapse a deliberate 4xx
403
+ into the built-in boundary's 500 fallback. `isHTTPError` tries
404
+ `instanceof` first, then falls back to a total structural check: the
405
+ value must carry a stable cross-copy brand (a `Symbol.for`-interned key,
406
+ so every copy resolves the same symbol) and expose a numeric `status` and
407
+ a string `message` — the exact fields the boundary reads off a
408
+ recognized error. The brand is an implementation detail of `HTTPError`'s
409
+ constructor, not a field a consumer sets by hand.
410
+ 19. **SSE producers can cooperate with real process-local transport
411
+ backpressure.** `StreamInterface.write` returns `true` only while the
412
+ underlying `ReadableStream` controller retains positive desired size after
413
+ accepting the event. A `false` result tells a cooperative producer to await
414
+ `drain()`; that promise parks without polling until a consumer pull restores
415
+ capacity, or stream closure settles the wait. Because the router response
416
+ pump stops pulling while `ServerResponse.write` is backpressured, a slow TCP
417
+ consumer makes this queue fill and the producer park. The signal remains
418
+ process-local — it does not prove remote receipt — and callers that ignore
419
+ it retain the original unconditional-enqueue behavior. The queue's default
420
+ strategy counts chunks, not their byte lengths, so a producer seeking a
421
+ byte bound must also cap each individual event.
422
+
423
+ ## Patterns
424
+
425
+ ### Quickstart: dispatcher, middleware, lifecycle
426
+
427
+ `createServer` takes a dispatcher and a per-request state factory, `use`
428
+ mounts middleware around the dispatch, and `start`, `stop`, and `destroy`
429
+ run the lifecycle.
430
+
431
+ ```ts
432
+ import type { MiddlewareHandler } from '@orkestrel/server'
433
+ import { createServer } from '@orkestrel/server'
434
+ import { createDispatcher } from '@orkestrel/router'
435
+
436
+ interface State {
437
+ readonly requestId: string
438
+ readonly ip: string | undefined
439
+ }
440
+
441
+ const dispatcher = createDispatcher<State>()
442
+ dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
443
+
444
+ const logRequestId: MiddlewareHandler<State> = async (_request, context, next) => {
445
+ const response = await next()
446
+ response.headers.set('X-Request-ID', context.state.requestId)
447
+ return response
448
+ }
449
+
450
+ const server = createServer<State>({
451
+ dispatcher,
452
+ state: (connection) => ({ requestId: crypto.randomUUID(), ip: connection.ip }),
453
+ })
454
+ server.use(logRequestId)
455
+ const port = await server.start()
456
+ await server.stop()
457
+ await server.destroy()
458
+ ```
459
+
460
+ ### Middleware ordering idiom
461
+
462
+ Middleware runs outermost-first (`middleware[0]` wraps everything after it).
463
+ A CORS handler must claim a preflight `OPTIONS` request before the
464
+ dispatcher's own auto-`OPTIONS` responder ever sees it — mount it earliest in
465
+ the array, ahead of anything that would short-circuit later.
466
+
467
+ ```ts
468
+ import type { MiddlewareHandler } from '@orkestrel/server'
469
+ import { createServer } from '@orkestrel/server'
470
+ import { createDispatcher } from '@orkestrel/router'
471
+
472
+ interface State {
473
+ readonly userId?: string
474
+ }
475
+
476
+ const cors: MiddlewareHandler<State> = async (request, _context, next) => {
477
+ if (request.method === 'OPTIONS') return new Response(null, { status: 204 })
478
+ return next()
479
+ }
480
+ const auth: MiddlewareHandler<State> = async (request, context, next) => {
481
+ return next(request)
482
+ }
483
+
484
+ const dispatcher = createDispatcher<State>()
485
+ const server = createServer<State>({
486
+ dispatcher,
487
+ state: () => ({}),
488
+ middleware: [cors, auth], // CORS claims preflights before dispatcher.handle's auto-OPTIONS
489
+ })
490
+ ```
491
+
492
+ ### Typed state slices
493
+
494
+ Each middleware family publishes its own state-slice interface; a consumer
495
+ intersects the slices it mounts into one `TState` — no per-middleware generic
496
+ accumulation.
497
+
498
+ ```ts
499
+ import type { MiddlewareHandler } from '@orkestrel/server'
500
+
501
+ interface TokenState {
502
+ readonly userId?: string
503
+ }
504
+ interface RequestIdState {
505
+ readonly requestId: string
506
+ }
507
+ type State = TokenState & RequestIdState
508
+
509
+ const withUser: MiddlewareHandler<State> = async (_request, context, next) => next()
510
+ ```
511
+
512
+ ### SSE route
513
+
514
+ A route returns the stream's `response` at once and pumps events into the
515
+ handle afterwards; a `write` that reports `false` is backpressure `drain`
516
+ waits out.
517
+
518
+ ```ts
519
+ import type { StreamInterface } from '@orkestrel/server'
520
+ import { createStream } from '@orkestrel/server'
521
+
522
+ async function pumpStream(stream: StreamInterface): Promise<void> {
523
+ if (!stream.write({ event: 'token', data: 'hello' })) await stream.drain()
524
+ stream.comment('keep-alive')
525
+ stream.end()
526
+ }
527
+
528
+ function streamHandler(): Response {
529
+ const stream = createStream()
530
+ void pumpStream(stream)
531
+ return stream.response
532
+ }
533
+ ```
534
+
535
+ ### Graceful shutdown
536
+
537
+ `stop()` refuses new connections, gives in-flight work up to the `drain`
538
+ deadline, then closes; `destroy()` is the final, idempotent teardown. In-flight
539
+ work is requests and claimed upgraded sockets, so each call always returns.
540
+
541
+ ```ts
542
+ import { createServer } from '@orkestrel/server'
543
+ import { createDispatcher } from '@orkestrel/router'
544
+
545
+ const dispatcher = createDispatcher()
546
+ const server = createServer({ dispatcher, state: () => ({}), drain: 5_000 })
547
+ server.emitter.on('drain', (pending, upgraded) =>
548
+ console.log(`drained with ${pending} requests and ${upgraded} sockets still open`),
549
+ )
550
+ await server.start()
551
+ await server.stop() // graceful — waits up to 5s for requests and upgraded sockets
552
+ await server.destroy() // idempotent final teardown
553
+ ```
554
+
555
+ A long-lived upgraded socket does not end by itself, so close it from the
556
+ `stop` event to keep the shutdown short. Without that the drain runs its whole
557
+ budget and the socket is cut instead.
558
+
559
+ ```ts
560
+ import type { Duplex } from 'node:stream'
561
+ import { createServer } from '@orkestrel/server'
562
+ import { createDispatcher } from '@orkestrel/router'
563
+
564
+ const live = new Set<Duplex>()
565
+ const server = createServer({ dispatcher: createDispatcher(), state: () => ({}) })
566
+ server.upgrade((_request, socket) => {
567
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n\r\n')
568
+ live.add(socket)
569
+ return true
570
+ })
571
+ server.emitter.on('stop', () => {
572
+ for (const socket of live) socket.end() // your protocol's clean goodbye
573
+ })
574
+ ```
575
+
576
+ ### Bounded startup and socket caps
577
+
578
+ `timeouts.start` bounds only listener startup; pass an `AbortSignal` to cancel
579
+ that same pending bind from the caller. Socket caps use one grouped option and
580
+ map directly to node's server properties.
581
+
582
+ ```ts
583
+ import { createServer } from '@orkestrel/server'
584
+ import { createDispatcher } from '@orkestrel/router'
585
+
586
+ const controller = new AbortController()
587
+ const server = createServer({
588
+ dispatcher: createDispatcher(),
589
+ state: () => ({}),
590
+ timeouts: { start: 5_000 },
591
+ sockets: { connections: 1_000, headers: 100, requests: 1_000 },
592
+ })
593
+
594
+ // Calling controller.abort() while startup is pending cancels this bind.
595
+ const port = await server.start(controller.signal)
596
+ ```
597
+
598
+ ### Upgrade attach
599
+
600
+ An upgrade handler returns `true` to claim the socket, which ends the
601
+ fan-out and leaves the connection with that handler.
602
+
603
+ ```ts
604
+ import { createServer } from '@orkestrel/server'
605
+ import { createDispatcher } from '@orkestrel/router'
606
+
607
+ const dispatcher = createDispatcher()
608
+ const server = createServer({ dispatcher, state: () => ({}) })
609
+ server.upgrade((_request, socket, _head) => {
610
+ if (socket.destroyed) return false
611
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n\r\n')
612
+ return true // claims the socket; a later handler never sees it
613
+ })
614
+ ```
615
+
616
+ ### Substrate direct use — tokens, cookies, negotiation
617
+
618
+ Each substrate helper stands on its own, so a caller reaches negotiation,
619
+ signed cookies, tokens, and capped decompression without a `Server`.
620
+
621
+ ```ts
622
+ import type { MiddlewareContext } from '@orkestrel/server'
623
+ import {
624
+ createNegotiator,
625
+ decodeTokenPayload,
626
+ decompressRequestBody,
627
+ readSignedCookie,
628
+ signToken,
629
+ verifyToken,
630
+ writeSignedCookie,
631
+ } from '@orkestrel/server'
632
+
633
+ declare const context: MiddlewareContext<Record<string, never>>
634
+
635
+ const negotiator = createNegotiator()
636
+ negotiator.negotiate('text/html, application/json;q=0.9', ['application/json', 'text/html']) // 'text/html'
637
+ negotiator.encoding('gzip;q=1.0, deflate;q=0.8', ['gzip', 'deflate']) // 'gzip'
638
+ negotiator.language('en-US, en;q=0.8, fr;q=0.5', ['en', 'fr']) // 'en'
639
+ await negotiator.format(new Request('http://x'), context, {
640
+ 'application/json': (_request, _context) => Response.json({ ok: true }),
641
+ })
642
+
643
+ const headers = new Headers()
644
+ await writeSignedCookie(headers, 'session', 'user-1', 'secret')
645
+ await readSignedCookie(
646
+ new Request('http://x', { headers: { cookie: 'session=abc' } }),
647
+ 'session',
648
+ 'secret',
649
+ )
650
+ await verifyToken('bad.token', 'secret') // undefined — total, never throws
651
+
652
+ const token = await signToken('client', { secret: 'shh' })
653
+ decodeTokenPayload(token.split('.')[0]) // 'client' — the shared decode step verifyToken applies after a signature match
654
+
655
+ const gzipped = new Uint8Array(
656
+ await new Response(
657
+ new Blob(['hi']).stream().pipeThrough(new CompressionStream('gzip')),
658
+ ).arrayBuffer(),
659
+ )
660
+ const body = await decompressRequestBody(gzipped, 'gzip', 1_048_576)
661
+ new TextDecoder().decode(body) // 'hi' — capped decompression, the zip-bomb defense
662
+ ```
663
+
664
+ ### Practices
665
+
666
+ - **The server consumes the router, never re-implements it** — bring your own
667
+ `DispatcherInterface`; this package owns zero route matching — mechanism,
668
+ not product policy.
669
+ - **Mount CORS before anything that could short-circuit an `OPTIONS`** — the
670
+ ordering idiom under [Middleware ordering idiom](#middleware-ordering-idiom);
671
+ the dispatcher's own auto-`OPTIONS` runs last.
672
+ - **Read `context.body()` through the cache, never `request.body` directly**
673
+ — the stream is drained exactly once, capped and zip-bomb-safe.
674
+ - **Thread `request.signal` into downstream work** — it fires on either
675
+ client disconnect or server `stop()`.
676
+ - **Never derive a rate key from `X-Forwarded-For`** — use the injected
677
+ `Connection.ip` (or your own trusted-proxy derivation).
678
+ - **Publish a state-slice interface per middleware family** — intersect the
679
+ slices a consumer mounts into one `TState`, never a generic-accumulating
680
+ chain.
681
+ - **`stop()` before `destroy()`** for a graceful shutdown; `destroy()` alone
682
+ is the abrupt final teardown, idempotent from any state.
683
+ - **Close your upgraded sockets on the `stop` event** — the server tracks them
684
+ so shutdown terminates, but only your handler speaks the protocol, so only
685
+ it can close one cleanly before the deadline cuts it.
686
+ - **Install a `report` sink for observability** — its own throw is swallowed,
687
+ so it can never crash a response.
688
+
689
+ ## Tests
690
+
691
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/server`
692
+ bijection (value and type exports), the `NegotiatorInterface` / `StreamInterface` /
693
+ `ServerInterface` ↔ implementing-class method bijections, and the equality gate:
694
+ every `Summary` cell against its declaration's description paragraph, the titled
695
+ `Substrate direct use — tokens, cookies, negotiation` fence against the `@example`
696
+ block of that title (pinned so the titled pair cannot be retired silently), and the
697
+ README pitch against this guide's tagline. It also runs the flagship fences and
698
+ asserts the values their comments claim.
699
+ - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) —
700
+ `compose` (outer-first ordering, double-`next` rejection, short-circuit,
701
+ request substitution, response transformation), cookie parse/serialize/
702
+ attribute-injection guards, `resolveSecure`, `clearCookie` (expiry and
703
+ accumulation), `computeCodingQuality`/`resolveCoding`/`negotiateEncoding`,
704
+ the `ETag` hex digest, and `discoverPort` (default, preferred, and
705
+ taken-preferred-falls-back cases).
706
+ - [`tests/src/server/validators.test.ts`](../tests/src/server/validators.test.ts) —
707
+ `isAddressInfo` narrowing over every shape `node:net`'s `server.address()`
708
+ returns.
709
+ - [`tests/src/server/Negotiator.test.ts`](../tests/src/server/Negotiator.test.ts) —
710
+ `negotiate`/`encoding`/`language`/`format`: exact vs subtype-wildcard vs
711
+ any-range precedence, `;q=0` rejection semantics, q-tie server-order
712
+ break, `format`'s 406 fallback and handler dispatch, the empty-header
713
+ divergence between `encoding` and `negotiate`, and the proof that
714
+ `encoding` and `negotiateEncoding` agree because they run one selection leaf.
715
+ - [`tests/src/server/Stream.test.ts`](../tests/src/server/Stream.test.ts) —
716
+ the opened SSE response and its header merge order, the serialized wire for
717
+ events and comments, readiness/drain and ignore-the-signal behavior, and the
718
+ ways the handle closes (`end`, and a consumer cancelling).
719
+ - [`tests/src/server/errors.test.ts`](../tests/src/server/errors.test.ts) —
720
+ `HTTPError`/`ContentTooLargeError` shape and `isHTTPError` narrowing, and
721
+ `ServerError` shape with `isServerError` narrowing.
722
+ - [`tests/src/server/factories.test.ts`](../tests/src/server/factories.test.ts) —
723
+ `createNegotiator`, `createServer`, and `createStream` round-trips + factory
724
+ return-type assertions, `createServer` option threading and construction
725
+ guards, and `createStream` option threading.
726
+ - [`tests/src/server/Server.test.ts`](../tests/src/server/Server.test.ts) —
727
+ the status matrix, restart-fresh-abort, caller-cancelled / timed-out / clean
728
+ bounded startup, `EADDRINUSE` honesty, host/port binds, ephemeral default,
729
+ connection / header / per-socket request caps, graceful-vs-forced drain,
730
+ the held-upgraded-socket stop (drained to the deadline then cut, settled
731
+ early when the claimant closes it, reported on `drain`, and force-closed by
732
+ `destroy()`) against a no-socket control, 20-parallel-none-dropped,
733
+ connection facts threaded into state,
734
+ `context.body()` caching, boundary mapping (`HTTPError`/other/`expose`), the
735
+ stop-signal-reaches-handlers case, and the real slow-TCP proof that an SSE
736
+ producer parks at local queue pressure, resumes on drain, and stays bounded
737
+ when cooperative while ignored readiness retains unconditional enqueue.
738
+
739
+ ## See also
740
+
741
+ - [`AGENTS.md`](../AGENTS.md) — the rules this package is written to, including
742
+ the design laws behind its emitter, its guards, and its
743
+ documentation-as-contract.
744
+ - [`router.md`](router.md) — `@orkestrel/router`, the dispatcher this server
745
+ consumes and never re-implements.
746
+ - [`abort.md`](abort.md) — `@orkestrel/abort`, the stop-signal/request-signal
747
+ linking primitive.
748
+ - [`emitter.md`](emitter.md) — `@orkestrel/emitter`, the `Server`'s lifecycle
749
+ event map.
750
+ - [`contract.md`](contract.md) — `@orkestrel/contract`, the guards backing
751
+ every construction boundary and untrusted read.
752
+ - [`README.md`](README.md) — the guides index.