@loopbacktools/server 0.0.0-stage → 0.4.0

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -2
  3. package/dist/package.json +57 -0
  4. package/dist/src/domain/ask.d.ts +78 -0
  5. package/dist/src/domain/ask.js +161 -0
  6. package/dist/src/domain/errors.d.ts +70 -0
  7. package/dist/src/domain/errors.js +57 -0
  8. package/dist/src/generated/app.gen.d.ts +2311 -0
  9. package/dist/src/generated/app.gen.js +400 -0
  10. package/dist/src/generated/runtime.gen.d.ts +357 -0
  11. package/dist/src/generated/runtime.gen.js +298 -0
  12. package/dist/src/generated/schemas.gen.d.ts +6416 -0
  13. package/dist/src/generated/schemas.gen.js +306 -0
  14. package/dist/src/generated/wire-contract.gen.d.ts +77 -0
  15. package/dist/src/generated/wire-contract.gen.js +3 -0
  16. package/dist/src/index.d.ts +11 -0
  17. package/dist/src/index.js +8 -0
  18. package/dist/src/loopback.d.ts +27 -0
  19. package/dist/src/loopback.js +16 -0
  20. package/dist/src/space/arguments.d.ts +8 -0
  21. package/dist/src/space/arguments.js +52 -0
  22. package/dist/src/space/migrations.d.ts +10 -0
  23. package/dist/src/space/migrations.js +106 -0
  24. package/dist/src/space/object.d.ts +30 -0
  25. package/dist/src/space/object.js +169 -0
  26. package/dist/src/space/operations.d.ts +149 -0
  27. package/dist/src/space/operations.js +568 -0
  28. package/dist/src/space/read.d.ts +8 -0
  29. package/dist/src/space/read.js +33 -0
  30. package/dist/src/space/rpc.d.ts +61 -0
  31. package/dist/src/space/rpc.js +96 -0
  32. package/dist/src/space/services.d.ts +41 -0
  33. package/dist/src/space/services.js +58 -0
  34. package/dist/src/space/store.d.ts +87 -0
  35. package/dist/src/space/store.js +276 -0
  36. package/dist/src/version.d.ts +2 -0
  37. package/dist/src/version.js +3 -0
  38. package/dist/src/worker/env.d.ts +19 -0
  39. package/dist/src/worker/env.js +3 -0
  40. package/dist/src/worker/files.d.ts +21 -0
  41. package/dist/src/worker/files.js +42 -0
  42. package/dist/src/worker/gate.d.ts +55 -0
  43. package/dist/src/worker/gate.js +83 -0
  44. package/dist/src/worker/http.d.ts +2092 -0
  45. package/dist/src/worker/http.js +291 -0
  46. package/dist/src/worker/jwt.d.ts +49 -0
  47. package/dist/src/worker/jwt.js +142 -0
  48. package/dist/src/worker/mcp.d.ts +15 -0
  49. package/dist/src/worker/mcp.js +401 -0
  50. package/dist/src/worker/run.d.ts +45 -0
  51. package/dist/src/worker/run.js +71 -0
  52. package/dist/src/worker/spaces.d.ts +24 -0
  53. package/dist/src/worker/spaces.js +64 -0
  54. package/package.json +51 -3
@@ -0,0 +1,357 @@
1
+ import type { Context, Env, Input, MiddlewareHandler } from "hono";
2
+ import type { output, ZodError, ZodType } from "zod";
3
+ /**
4
+ * One arm of an operation's declared response set, as the document publishes it.
5
+ *
6
+ * Declared here rather than imported from `typespec-http-zod`, so that this module and the copy the
7
+ * emitter writes beside the generated code are both free of any package import at run time. The two
8
+ * declarations are structurally identical, which is all TypeScript requires, and
9
+ * typespec-hono's runtime parity suite asserts that this `armFor` answers identically to the library's for
10
+ * every shape of arm list.
11
+ */
12
+ export interface ResponseArm {
13
+ readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
14
+ readonly schema: ZodType | undefined;
15
+ /**
16
+ * Every media type this response offers, including a single one. Absent where there is no body.
17
+ */
18
+ readonly contentTypes?: readonly string[];
19
+ /**
20
+ * The headers this response declares, by the WIRE name the response sets. `optional` is the
21
+ * document's `required: false`. Absent where the response declares none.
22
+ */
23
+ readonly headers?: readonly {
24
+ readonly name: string;
25
+ readonly optional: boolean;
26
+ }[];
27
+ }
28
+ /**
29
+ * The arm that applies to a status, preferring an exact code, then its `NXX` range, then `default`.
30
+ *
31
+ * That order is the document's own: OpenAPI resolves a response the same way, so an application
32
+ * choosing an arm by hand would have to re-derive this and could get it wrong differently.
33
+ */
34
+ export declare function armFor(arms: readonly ResponseArm[], status: number): ResponseArm | undefined;
35
+ /**
36
+ * One acceptable combination of credentials, exactly as OpenAPI's `security` states it: scheme id to
37
+ * the scopes that scheme requires. Every entry in one requirement must be satisfied TOGETHER, and
38
+ * satisfying ANY ONE requirement authorises the caller.
39
+ *
40
+ * **Declared here rather than beside the code that derives it**, because `./runtime` is what a
41
+ * running server imports and must stay free of every build-time dependency, a packaging arm asserts
42
+ * it names no `@typespec/*` package at all. An application should not drag a compiler into its
43
+ * Worker to read one type.
44
+ */
45
+ export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
46
+ /**
47
+ * The contract between the GENERATED server and the app that mounts it.
48
+ *
49
+ * **This exists because "here is a data table, write your own router" is not a deliverable.**
50
+ * A table of routes leaves every application to hand-write the loop that interprets it at run time.
51
+ * That loop sits outside every oracle the emitter is judged by, and it needs a cast that exists
52
+ * *only* because iterating a homogeneous array throws away the per-operation types the emitter knew:
53
+ * `backend[operationId]` becomes a union of every operation's method, so nothing about the call can
54
+ * be checked. Generating the server removes the loop and the cast.
55
+ *
56
+ * What is left for the app to supply is genuinely app-specific: how a request becomes a caller's
57
+ * context, and what a refusal looks like. Everything else, routing, validation, which validator
58
+ * applies to which target, which statuses an operation may answer with and how each one is served,
59
+ * is generated.
60
+ */
61
+ /**
62
+ * The Hono environment the generated server mounts on.
63
+ *
64
+ * **An interface, so an application AUGMENTS it rather than replacing this module.**
65
+ *
66
+ * ```ts
67
+ * declare module "./generated/runtime.gen.js" {
68
+ * interface AppEnv {
69
+ * Bindings: { BACKEND: Service<Backend> };
70
+ * Variables: { principal: Principal };
71
+ * }
72
+ * }
73
+ * ```
74
+ *
75
+ * That is the idiom Hono itself uses for `ContextVariableMap`, and it is what lets this module be
76
+ * emitted beside the generated code on every compile instead of being copied into an application and
77
+ * aged there. A copy is the only other way to name an environment, and a copy falls behind the
78
+ * emitter that generates the code beside it, keeping every defect fixed since it was taken.
79
+ *
80
+ * **Concrete rather than a type parameter of `registerRoutes`, and that was measured twice.** Hono
81
+ * narrows `Context` per route, and its conditional types cannot reduce
82
+ * `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing an
83
+ * application supplies is ever assignable and every call site needs a cast. Confirmed again on hono
84
+ * 4.13.1 with TypeScript 7.0.2: three `TS2345`s on a three-route probe.
85
+ */
86
+ export interface AppEnv extends Env {
87
+ }
88
+ /** Anything an operation may hand back: the value, or a promise of it. */
89
+ export type Awaitable<T> = T | Promise<T>;
90
+ /**
91
+ * Pick the media type to serve, per RFC 9110 section 12.5.1.
92
+ *
93
+ * **In the runtime rather than in {@link RouteDeps}, deliberately.** The test for admitting
94
+ * anything to `deps` is *the generated code cannot proceed without an answer*, and this is not that:
95
+ * which media types an operation offers is a contract fact the emitter reads from the document, and
96
+ * how `Accept` selects among them is specified by the RFC. Both sides are derivable, so an app that
97
+ * had to supply this would be re-implementing the standard, and could get it wrong differently from
98
+ * everybody else.
99
+ *
100
+ * The rules that matter, and that a naive `includes()` gets wrong:
101
+ * - **absent or empty `Accept` means anything is acceptable**, serve the first offer;
102
+ * - **specificity SELECTS which rule applies, before quality is read at all.** For each offered
103
+ * type, the most specific range that matches it decides its quality: an exact type beats a
104
+ * subtype wildcard (`text/*`), which beats the fully wildcard range. The fully wildcard range is
105
+ * not written literally here because it would close this comment;
106
+ * - **`q=0` is a REFUSAL**, not a weak preference, so a type whose applicable rule scores zero is
107
+ * never chosen;
108
+ * - equal quality keeps the order the document offers, so nothing in the header displaces it;
109
+ * - a malformed `q` is IGNORED rather than read as zero - a typo should not turn into a 406;
110
+ * - parameters after the media range (`;charset=utf-8`) are not part of the match.
111
+ *
112
+ * **Specificity is not a tie-break.** Scoring every matching range and keeping the best
113
+ * `(q, specificity)` pair lets a permissive wildcard out-vote the precise rule a caller wrote about
114
+ * that exact type, so an `Accept` of any type at all followed by `application/json;q=0` would be
115
+ * served JSON, which is the one outcome an explicit refusal must never produce. typespec-hono's
116
+ * negotiation suite holds this function to the rules above.
117
+ *
118
+ * Returns `undefined` when nothing offered is acceptable. The caller answers 406, and the
119
+ * difference between "no preference" and "no acceptable option" is exactly what that turns on.
120
+ */
121
+ export declare function selectContentType(accept: string | undefined, offered: readonly string[]): string | undefined;
122
+ /**
123
+ * Let only a real HEAD request through.
124
+ *
125
+ * Hono rewrites HEAD to GET before matching, so a `@head` operation has to be registered under GET
126
+ * to be reachable at all. Where the document declares no GET on that path, this keeps the
127
+ * registration honest: a GET gets the 404 it would have got if the route had never been registered,
128
+ * and only a HEAD reaches the validators and the handler. `c.req.method` still reads `HEAD` after the
129
+ * rewrite, which is what makes the distinction possible.
130
+ *
131
+ * In the runtime rather than in {@link RouteDeps} on the usual test: the generated code can proceed
132
+ * without asking the app anything. Which verbs the document declares is a contract fact, and the
133
+ * answer for a verb it does not declare is the same 404 any unrouted request already gets, through
134
+ * whatever `app.notFound()` the application has set.
135
+ *
136
+ * A plain middleware rather than `except()` from `hono/combine`, because `except` wraps the final
137
+ * handler and erases its response type, and Hono's RPC client derives its whole surface from that
138
+ * type. Measured: `hc<typeof app>` resolved a wrapped route's body to `unknown`.
139
+ */
140
+ export declare const headOnly: MiddlewareHandler;
141
+ /**
142
+ * **A route whose template writes a query string, `/items?fixed=true{&param}`, is only that route when
143
+ * the request carries it.** A router matches paths, so the pairs are checked here, and a request
144
+ * without them gets the 404 any unrouted request gets, through whatever `app.notFound()` the
145
+ * application has set. In the runtime for the same reason as {@link headOnly}.
146
+ */
147
+ export declare const literalQuery: (pairs: readonly (readonly [string, string])[]) => MiddlewareHandler;
148
+ /**
149
+ * A response body that does not match the schema the document publishes for its status.
150
+ *
151
+ * **Thrown, so an application decides what a contract failure answers with in `app.onError`**,
152
+ * which is where Hono puts that decision: a 500, a 502, a 502 with the issues in the body or with
153
+ * them redacted. The check itself is the same for every application, and it covers failure bodies
154
+ * as well as successful ones.
155
+ *
156
+ * `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
157
+ * application that logs them should decide whether its responses may contain anything it would not
158
+ * log.
159
+ */
160
+ export declare class ResponseContractError extends Error {
161
+ readonly operationId: string;
162
+ readonly status: number;
163
+ readonly issues: ZodError["issues"];
164
+ /**
165
+ * Which half of the response broke its contract. **Defaulted, so the three-argument form an
166
+ * application may already construct or match on keeps working.**
167
+ */
168
+ readonly part: "body" | "headers";
169
+ /**
170
+ * Fields declared and assigned rather than written as parameter properties, which are not
171
+ * erasable syntax: a project building with `erasableSyntaxOnly` could not compile this file.
172
+ */
173
+ constructor(operationId: string, status: number, issues: ZodError["issues"], part?: "body" | "headers");
174
+ }
175
+ /**
176
+ * A handler answered with a status its operation does not declare.
177
+ *
178
+ * **Unreachable from a typed handler.** The generated `Operations` interface types every result as
179
+ * the union of the declared statuses, so a literal outside it does not compile. This is what a CAST
180
+ * produces, or a result that crossed a boundary the type system cannot see into, such as untyped
181
+ * data from a service binding. Thrown rather than served, because serving it would publish a status
182
+ * the contract does not state.
183
+ */
184
+ export declare class UndeclaredStatusError extends Error {
185
+ readonly operationId: string;
186
+ readonly result: unknown;
187
+ constructor(operationId: string, result: unknown);
188
+ }
189
+ /**
190
+ * **A stream of bytes, as every platform's `ReadableStream<Uint8Array>` satisfies it.**
191
+ *
192
+ * The same structural type `typespec-http-zod` publishes in `requests.gen.ts`, so a backend whose
193
+ * result is typed by the framework-free contract can be returned from a handler as it is: a contract
194
+ * cannot name `ReadableStream` without a DOM or runtime library, so it states the reader every stream
195
+ * has. Declared here rather than imported because this file is copied beside the generated code, into
196
+ * a Worker where the emitter package is not installed; the corpus agreement test holds the two
197
+ * identical.
198
+ */
199
+ export interface ByteStream {
200
+ getReader(): {
201
+ read(): Promise<{
202
+ readonly done: boolean;
203
+ readonly value?: Uint8Array | undefined;
204
+ }>;
205
+ releaseLock(): void;
206
+ cancel?(reason?: unknown): Promise<void>;
207
+ };
208
+ }
209
+ /**
210
+ * The bytes a binary or streamed response SERVES, as something `c.body` accepts.
211
+ *
212
+ * **A platform stream, a `Uint8Array` or an `ArrayBuffer` is served as it is** - a stream keeps the
213
+ * platform's own piping, which is the point of handing over a stream at all. Anything else satisfying
214
+ * `ByteStream` is pulled through a `ReadableStream`, one chunk per read, so it still streams rather
215
+ * than being read whole. Without this, `new Response(value)` stringifies an object it does not
216
+ * recognise, and the body served is `[object Object]` with a 200.
217
+ */
218
+ export declare function servedBytes(body: ByteStream | Uint8Array<ArrayBuffer> | ArrayBuffer): ReadableStream<Uint8Array> | Uint8Array<ArrayBuffer> | ArrayBuffer;
219
+ /**
220
+ * The body a response SERVES: what the handler returned, parsed against the schema the document
221
+ * publishes for that status.
222
+ *
223
+ * **What is served is the PARSED value, not the one the handler returned.** A schema that strips
224
+ * undeclared keys therefore strips them from the wire too, which is how an internal field such as a
225
+ * password hash is kept out of a response the document does not publish it in.
226
+ *
227
+ * Synchronous, because nothing this emitter writes is asynchronous - typespec-hono's suite asserts it
228
+ * over the whole corpus - and the asynchronous path costs 2.6x per parse.
229
+ */
230
+ export declare function servedBody<S extends ZodType>(schema: S, value: unknown, operationId: string, status: number): output<S>;
231
+ /**
232
+ * Declared response headers, as the strings a response carries.
233
+ *
234
+ * An optional header the handler did not supply is omitted rather than sent as `"undefined"`, and a
235
+ * typed value - a `retry-after` declared `int32` - is written as its text. Hono's `HeaderRecord`
236
+ * refuses `undefined`, so dropping it here is also what keeps the generated call sites free of a
237
+ * conditional per header.
238
+ */
239
+ export declare function headersOf(declared: Readonly<Record<string, unknown>>): Record<string, string>;
240
+ /**
241
+ * Declared response headers, CHECKED against the schemas the document publishes for them.
242
+ *
243
+ * **Checked as the body beside them is.** `servedBody` parses every response body against its
244
+ * status's schema and throws `ResponseContractError`; `headersOf` alone is `String(value)` and
245
+ * nothing else, so a header the document constrains to `^[A-Za-z0-9-]+$` would be served as
246
+ * `not a valid id"; injected=1` with a 200, while a body breaking its own schema on the same route
247
+ * throws. That asymmetry is the whole of what this removes.
248
+ *
249
+ * **Not a response-splitting hole, and worth saying so.** `new Response` refuses a header value
250
+ * containing CR or LF with a `TypeError` on both `workerd` and undici, so the runtime already stops
251
+ * the injection. What it does not stop is a response that disagrees with its own document, which a
252
+ * client generated from that document is entitled to rely on.
253
+ *
254
+ * **Undefined values are dropped BEFORE the check, not after.** An optional header the handler did
255
+ * not supply is absent rather than explicitly `undefined`, which is what the emitted schema's
256
+ * `.exactOptional()` means, and it is also what Hono's `HeaderRecord` requires. The check then runs
257
+ * on the values as the handler produced them - a `retry-after` declared `int32` is still a number
258
+ * here - because checking the text would check a different thing than the document describes.
259
+ *
260
+ * **What is returned is every header, including ones the schema does not mention.** A response
261
+ * carries headers the document does not declare, the `Content-Type` this emitter sets among them,
262
+ * and those are not a contract violation. The schema is a plain object schema, so it ignores them.
263
+ */
264
+ export declare function servedHeaders(schema: ZodType, declared: Readonly<Record<string, unknown>>, operationId: string, status: number): Record<string, string>;
265
+ /**
266
+ * Whether a media type a handler answers with lies inside a range the document offers.
267
+ *
268
+ * **A range is not a type a response can be sent as**, so where a status offers `image/*` the handler
269
+ * names the concrete type, and its result type already refuses one outside the range. This is what a
270
+ * CAST reaches, or data from a service binding: `text/html` under `image/*`, or `image/*` itself,
271
+ * would otherwise be served with a `Content-Type` the document does not permit. Type names compare
272
+ * case-insensitively (RFC 9110 section 8.3.1), and parameters such as `charset` are allowed.
273
+ */
274
+ export declare function mediaTypeWithin(served: string, range: string): boolean;
275
+ /**
276
+ * What the app provides. One object, passed once, rather than a module the generated file imports by
277
+ * path. A generated server that hard-codes `../../backend.js` is only usable by the project it was
278
+ * generated in, and this one has to be usable by any.
279
+ *
280
+ * **The hooks are generic over Hono's path and input parameters, deliberately.** Hono narrows
281
+ * `Context` per route, by the literal path, and by whatever the validators on that route produced,
282
+ * so a hook typed against a single `Context<E>` is not assignable at any real call site. Making the
283
+ * hooks generic lets the app write functions that ignore both, without a cast anywhere.
284
+ *
285
+ * **`C` is the caller context, and `registerRoutes` INFERS it from `context`.** An application that
286
+ * returns a `Caller` from `context` has handlers typed `(ctx: Caller, input)`, with nothing to
287
+ * declare.
288
+ *
289
+ * **Nothing here renders a successful response, and that is the point.** Which statuses an operation
290
+ * may answer with, which body each one carries and how it is serialised are all things the document
291
+ * states, so the generated route does them. A hook handed every arm and the handler's result would
292
+ * leave every application to re-implement that choice by hand, in a status table nothing checks
293
+ * against the declared arms. What remains are the refusals that happen before a handler runs, whose
294
+ * envelope only the application knows.
295
+ */
296
+ export interface RouteDeps<E extends Env = AppEnv, C = unknown> {
297
+ /**
298
+ * The gate the DOCUMENT publishes, as middleware.
299
+ *
300
+ * **Which scopes an operation demands is a contract fact; how a token is verified is not.**
301
+ * `@useAuth(OAuth2Auth<...>)` reaches OpenAPI as `security` per operation, so the requirement is
302
+ * generated and this implements the check. The same split as `context`. Emitted
303
+ * only where the operation declares scopes, which is why an internal surface with none is
304
+ * unaffected. Without it, a surface whose document declares scopes would mount with its gate
305
+ * silently dropped.
306
+ *
307
+ * **It receives the document's REQUIREMENTS, not a flat list of scopes.** `@useAuth(BearerAuth)`
308
+ * publishes `security: [{ "BearerAuth": [] }]` with no scopes, so a scopes-only gate would cover
309
+ * OAuth2 and nothing else. Bearer, api-key and basic, which is most services, would carry no gate
310
+ * at all and rest entirely on `context` returning null, and an app whose `context` read a cookie
311
+ * would serve a route the document says needs a bearer token.
312
+ *
313
+ * Satisfying ANY ONE requirement authorises the caller, and every scheme WITHIN a requirement must
314
+ * be satisfied together, which is exactly what an array of OpenAPI `security` objects means.
315
+ */
316
+ readonly authorize: (requirements: readonly SecurityRequirement[]) => MiddlewareHandler<E>;
317
+ /**
318
+ * The caller's context, or `null` when there is none to establish.
319
+ *
320
+ * `authentication` is what the DOCUMENT says, and only that:
321
+ *
322
+ * - `"none"`: no requirement asks for anything (`@useAuth(NoAuth)`, or no authentication at all);
323
+ * - `"optional"`: an anonymous alternative sits beside a real one (`NoAuth | BearerAuth`), so
324
+ * `authorize` has admitted this caller either way and a presented credential should still be
325
+ * read;
326
+ * - `"required"`: every alternative asks for something.
327
+ *
328
+ * **Without `"optional"` the middle case would read as `"none"`**, so a caller with a valid token
329
+ * on an optional route would never be established as a caller. Deciding it at generation time is
330
+ * the point: the gate the document publishes is the gate that runs.
331
+ *
332
+ * **Nothing finer than the document states.** A generated server enforcing a rule derived from
333
+ * nothing published is the defect class this emitter exists to remove. An app that needs a finer
334
+ * distinction between callers can read the request, which is the one thing it definitely has.
335
+ */
336
+ readonly context: <P extends string, I extends Input>(c: Context<E, P, I>, authentication: "none" | "optional" | "required") => C | null;
337
+ /** The response when `context` returns `null`. */
338
+ readonly noContext: <P extends string, I extends Input>(c: Context<E, P, I>) => Response;
339
+ /**
340
+ * The response when the caller's `Accept` matches nothing the operation offers, a 406.
341
+ *
342
+ * Emitted only on routes where the document declares more than one media type for a status, so
343
+ * a service without content negotiation never sees it. Same shape of hook as {@link noContext}
344
+ * and admitted on the same test: the status and the `offered` list are contract facts the
345
+ * generated code already has, but the body they are reported in is the app's envelope, and it
346
+ * cannot proceed without one. {@link selectContentType} does the choosing; this reports failure.
347
+ */
348
+ readonly notAcceptable: <P extends string, I extends Input>(c: Context<E, P, I>, offered: readonly string[]) => Response;
349
+ /**
350
+ * Passed straight to `zValidator`'s hook. Returning `undefined` lets a successful validation
351
+ * through; returning a `Response` is how a rejection becomes the status this API promises rather
352
+ * than the middleware's default.
353
+ */
354
+ readonly invalid: <P extends string, I extends Input>(result: {
355
+ readonly success: boolean;
356
+ }, c: Context<E, P, I>) => Response | undefined;
357
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The arm that applies to a status, preferring an exact code, then its `NXX` range, then `default`.
3
+ *
4
+ * That order is the document's own: OpenAPI resolves a response the same way, so an application
5
+ * choosing an arm by hand would have to re-derive this and could get it wrong differently.
6
+ */
7
+ export function armFor(arms, status) {
8
+ return (arms.find((arm) => arm.status === status) ??
9
+ arms.find((arm) => arm.status === `${Math.floor(status / 100)}XX`) ??
10
+ arms.find((arm) => arm.status === "default"));
11
+ }
12
+ /**
13
+ * Pick the media type to serve, per RFC 9110 section 12.5.1.
14
+ *
15
+ * **In the runtime rather than in {@link RouteDeps}, deliberately.** The test for admitting
16
+ * anything to `deps` is *the generated code cannot proceed without an answer*, and this is not that:
17
+ * which media types an operation offers is a contract fact the emitter reads from the document, and
18
+ * how `Accept` selects among them is specified by the RFC. Both sides are derivable, so an app that
19
+ * had to supply this would be re-implementing the standard, and could get it wrong differently from
20
+ * everybody else.
21
+ *
22
+ * The rules that matter, and that a naive `includes()` gets wrong:
23
+ * - **absent or empty `Accept` means anything is acceptable**, serve the first offer;
24
+ * - **specificity SELECTS which rule applies, before quality is read at all.** For each offered
25
+ * type, the most specific range that matches it decides its quality: an exact type beats a
26
+ * subtype wildcard (`text/*`), which beats the fully wildcard range. The fully wildcard range is
27
+ * not written literally here because it would close this comment;
28
+ * - **`q=0` is a REFUSAL**, not a weak preference, so a type whose applicable rule scores zero is
29
+ * never chosen;
30
+ * - equal quality keeps the order the document offers, so nothing in the header displaces it;
31
+ * - a malformed `q` is IGNORED rather than read as zero - a typo should not turn into a 406;
32
+ * - parameters after the media range (`;charset=utf-8`) are not part of the match.
33
+ *
34
+ * **Specificity is not a tie-break.** Scoring every matching range and keeping the best
35
+ * `(q, specificity)` pair lets a permissive wildcard out-vote the precise rule a caller wrote about
36
+ * that exact type, so an `Accept` of any type at all followed by `application/json;q=0` would be
37
+ * served JSON, which is the one outcome an explicit refusal must never produce. typespec-hono's
38
+ * negotiation suite holds this function to the rules above.
39
+ *
40
+ * Returns `undefined` when nothing offered is acceptable. The caller answers 406, and the
41
+ * difference between "no preference" and "no acceptable option" is exactly what that turns on.
42
+ */
43
+ export function selectContentType(accept, offered) {
44
+ if (offered.length === 0)
45
+ return undefined;
46
+ const header = accept?.trim();
47
+ if (header === undefined || header === "")
48
+ return offered[0];
49
+ const ranges = header.split(",").map((entry) => {
50
+ const [range = "", ...parameters] = entry.split(";").map((part) => part.trim());
51
+ const quality = parameters
52
+ .map((parameter) => /^q=(?<value>[\d.]+)$/i.exec(parameter)?.groups?.["value"])
53
+ .find((value) => value !== undefined);
54
+ const q = quality === undefined ? 1 : Number(quality);
55
+ // A malformed `q` is treated as unstated. Reading `q=1.2.3` as zero would 406 a typo.
56
+ return { range: range.toLowerCase(), q: Number.isFinite(q) ? q : 1 };
57
+ });
58
+ let best;
59
+ for (const type of offered) {
60
+ // Compared by its essence, as the ranges are: a parameter on an offered type is not a type.
61
+ const lowered = (type.split(";")[0] ?? "").trim().toLowerCase();
62
+ const [group] = lowered.split("/");
63
+ /**
64
+ * **The most specific matching range decides this type's quality**, which is what makes an
65
+ * explicit `application/json;q=0` beat a wildcard that would otherwise accept it. Reading the
66
+ * best-scoring range instead lets a permissive rule override a precise one.
67
+ */
68
+ let applicable;
69
+ for (const { range, q } of ranges) {
70
+ const specificity = range === lowered ? 2 : range === `${group}/*` ? 1 : range === "*/*" ? 0 : -1;
71
+ if (specificity < 0)
72
+ continue;
73
+ // Strictly greater, so two rules of equal specificity leave the first one in force.
74
+ if (applicable === undefined || specificity > applicable.specificity) {
75
+ applicable = { q, specificity };
76
+ }
77
+ }
78
+ // `q=0` is "I will not accept this", so a type its own rule scores zero is never a candidate.
79
+ if (applicable === undefined || applicable.q <= 0)
80
+ continue;
81
+ // Strictly greater, so equal quality keeps the order the document offers.
82
+ if (best === undefined || applicable.q > best.q)
83
+ best = { type, q: applicable.q };
84
+ }
85
+ return best?.type;
86
+ }
87
+ /**
88
+ * Let only a real HEAD request through.
89
+ *
90
+ * Hono rewrites HEAD to GET before matching, so a `@head` operation has to be registered under GET
91
+ * to be reachable at all. Where the document declares no GET on that path, this keeps the
92
+ * registration honest: a GET gets the 404 it would have got if the route had never been registered,
93
+ * and only a HEAD reaches the validators and the handler. `c.req.method` still reads `HEAD` after the
94
+ * rewrite, which is what makes the distinction possible.
95
+ *
96
+ * In the runtime rather than in {@link RouteDeps} on the usual test: the generated code can proceed
97
+ * without asking the app anything. Which verbs the document declares is a contract fact, and the
98
+ * answer for a verb it does not declare is the same 404 any unrouted request already gets, through
99
+ * whatever `app.notFound()` the application has set.
100
+ *
101
+ * A plain middleware rather than `except()` from `hono/combine`, because `except` wraps the final
102
+ * handler and erases its response type, and Hono's RPC client derives its whole surface from that
103
+ * type. Measured: `hc<typeof app>` resolved a wrapped route's body to `unknown`.
104
+ */
105
+ export const headOnly = async (c, next) => c.req.method === "HEAD" ? next() : c.notFound();
106
+ /**
107
+ * **A route whose template writes a query string, `/items?fixed=true{&param}`, is only that route when
108
+ * the request carries it.** A router matches paths, so the pairs are checked here, and a request
109
+ * without them gets the 404 any unrouted request gets, through whatever `app.notFound()` the
110
+ * application has set. In the runtime for the same reason as {@link headOnly}.
111
+ */
112
+ export const literalQuery = (pairs) => async (c, next) => pairs.every(([name, value]) => c.req.query(name) === value) ? next() : c.notFound();
113
+ /**
114
+ * A response body that does not match the schema the document publishes for its status.
115
+ *
116
+ * **Thrown, so an application decides what a contract failure answers with in `app.onError`**,
117
+ * which is where Hono puts that decision: a 500, a 502, a 502 with the issues in the body or with
118
+ * them redacted. The check itself is the same for every application, and it covers failure bodies
119
+ * as well as successful ones.
120
+ *
121
+ * `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
122
+ * application that logs them should decide whether its responses may contain anything it would not
123
+ * log.
124
+ */
125
+ export class ResponseContractError extends Error {
126
+ operationId;
127
+ status;
128
+ issues;
129
+ /**
130
+ * Which half of the response broke its contract. **Defaulted, so the three-argument form an
131
+ * application may already construct or match on keeps working.**
132
+ */
133
+ part;
134
+ /**
135
+ * Fields declared and assigned rather than written as parameter properties, which are not
136
+ * erasable syntax: a project building with `erasableSyntaxOnly` could not compile this file.
137
+ */
138
+ constructor(operationId, status, issues, part = "body") {
139
+ super(`${operationId} answered ${status} with ${part === "body" ? "a body" : "headers"} its document does not permit`);
140
+ this.name = "ResponseContractError";
141
+ this.operationId = operationId;
142
+ this.status = status;
143
+ this.issues = issues;
144
+ this.part = part;
145
+ }
146
+ }
147
+ /**
148
+ * A handler answered with a status its operation does not declare.
149
+ *
150
+ * **Unreachable from a typed handler.** The generated `Operations` interface types every result as
151
+ * the union of the declared statuses, so a literal outside it does not compile. This is what a CAST
152
+ * produces, or a result that crossed a boundary the type system cannot see into, such as untyped
153
+ * data from a service binding. Thrown rather than served, because serving it would publish a status
154
+ * the contract does not state.
155
+ */
156
+ export class UndeclaredStatusError extends Error {
157
+ operationId;
158
+ result;
159
+ constructor(operationId, result) {
160
+ const status = typeof result === "object" && result !== null && "status" in result
161
+ ? String(result.status)
162
+ : "no status";
163
+ super(`${operationId} answered ${status}, which its document does not declare`);
164
+ this.operationId = operationId;
165
+ this.result = result;
166
+ this.name = "UndeclaredStatusError";
167
+ }
168
+ }
169
+ /**
170
+ * The bytes a binary or streamed response SERVES, as something `c.body` accepts.
171
+ *
172
+ * **A platform stream, a `Uint8Array` or an `ArrayBuffer` is served as it is** - a stream keeps the
173
+ * platform's own piping, which is the point of handing over a stream at all. Anything else satisfying
174
+ * `ByteStream` is pulled through a `ReadableStream`, one chunk per read, so it still streams rather
175
+ * than being read whole. Without this, `new Response(value)` stringifies an object it does not
176
+ * recognise, and the body served is `[object Object]` with a 200.
177
+ */
178
+ export function servedBytes(body) {
179
+ if (body instanceof ReadableStream || body instanceof Uint8Array || body instanceof ArrayBuffer) {
180
+ return body;
181
+ }
182
+ const reader = body.getReader();
183
+ return new ReadableStream({
184
+ /**
185
+ * **Reads until there is a chunk to hand on.** A read may report none and not be done, which
186
+ * `ByteStream` admits; returning without enqueueing anything left the stream waiting for a
187
+ * pull nothing would make, so a server reading it as a `Response` never finished.
188
+ */
189
+ async pull(controller) {
190
+ for (;;) {
191
+ const { done, value } = await reader.read();
192
+ if (done) {
193
+ reader.releaseLock();
194
+ controller.close();
195
+ return;
196
+ }
197
+ if (value !== undefined) {
198
+ controller.enqueue(value);
199
+ return;
200
+ }
201
+ }
202
+ },
203
+ /** The caller went away, so the producer is told: releasing the reader alone left it running. */
204
+ async cancel(reason) {
205
+ await reader.cancel?.(reason);
206
+ reader.releaseLock();
207
+ },
208
+ });
209
+ }
210
+ /**
211
+ * The body a response SERVES: what the handler returned, parsed against the schema the document
212
+ * publishes for that status.
213
+ *
214
+ * **What is served is the PARSED value, not the one the handler returned.** A schema that strips
215
+ * undeclared keys therefore strips them from the wire too, which is how an internal field such as a
216
+ * password hash is kept out of a response the document does not publish it in.
217
+ *
218
+ * Synchronous, because nothing this emitter writes is asynchronous - typespec-hono's suite asserts it
219
+ * over the whole corpus - and the asynchronous path costs 2.6x per parse.
220
+ */
221
+ export function servedBody(schema, value, operationId, status) {
222
+ const parsed = schema.safeParse(value);
223
+ if (!parsed.success)
224
+ throw new ResponseContractError(operationId, status, parsed.error.issues);
225
+ return parsed.data;
226
+ }
227
+ /**
228
+ * Declared response headers, as the strings a response carries.
229
+ *
230
+ * An optional header the handler did not supply is omitted rather than sent as `"undefined"`, and a
231
+ * typed value - a `retry-after` declared `int32` - is written as its text. Hono's `HeaderRecord`
232
+ * refuses `undefined`, so dropping it here is also what keeps the generated call sites free of a
233
+ * conditional per header.
234
+ */
235
+ export function headersOf(declared) {
236
+ const headers = {};
237
+ for (const [name, value] of Object.entries(declared)) {
238
+ if (value !== undefined)
239
+ headers[name] = String(value);
240
+ }
241
+ return headers;
242
+ }
243
+ /**
244
+ * Declared response headers, CHECKED against the schemas the document publishes for them.
245
+ *
246
+ * **Checked as the body beside them is.** `servedBody` parses every response body against its
247
+ * status's schema and throws `ResponseContractError`; `headersOf` alone is `String(value)` and
248
+ * nothing else, so a header the document constrains to `^[A-Za-z0-9-]+$` would be served as
249
+ * `not a valid id"; injected=1` with a 200, while a body breaking its own schema on the same route
250
+ * throws. That asymmetry is the whole of what this removes.
251
+ *
252
+ * **Not a response-splitting hole, and worth saying so.** `new Response` refuses a header value
253
+ * containing CR or LF with a `TypeError` on both `workerd` and undici, so the runtime already stops
254
+ * the injection. What it does not stop is a response that disagrees with its own document, which a
255
+ * client generated from that document is entitled to rely on.
256
+ *
257
+ * **Undefined values are dropped BEFORE the check, not after.** An optional header the handler did
258
+ * not supply is absent rather than explicitly `undefined`, which is what the emitted schema's
259
+ * `.exactOptional()` means, and it is also what Hono's `HeaderRecord` requires. The check then runs
260
+ * on the values as the handler produced them - a `retry-after` declared `int32` is still a number
261
+ * here - because checking the text would check a different thing than the document describes.
262
+ *
263
+ * **What is returned is every header, including ones the schema does not mention.** A response
264
+ * carries headers the document does not declare, the `Content-Type` this emitter sets among them,
265
+ * and those are not a contract violation. The schema is a plain object schema, so it ignores them.
266
+ */
267
+ export function servedHeaders(schema, declared, operationId, status) {
268
+ const present = {};
269
+ for (const [name, value] of Object.entries(declared)) {
270
+ if (value !== undefined)
271
+ present[name] = value;
272
+ }
273
+ const parsed = schema.safeParse(present);
274
+ if (!parsed.success) {
275
+ throw new ResponseContractError(operationId, status, parsed.error.issues, "headers");
276
+ }
277
+ return headersOf(present);
278
+ }
279
+ /** RFC 9110 `token`, less `*`, which names a range rather than a type. */
280
+ const MEDIA_TOKEN = "[!#$%&'+.^_`|~0-9A-Za-z-]+";
281
+ const SERVED_MEDIA_TYPE = new RegExp(`^(${MEDIA_TOKEN})/${MEDIA_TOKEN}\\s*(;.*)?$`);
282
+ /**
283
+ * Whether a media type a handler answers with lies inside a range the document offers.
284
+ *
285
+ * **A range is not a type a response can be sent as**, so where a status offers `image/*` the handler
286
+ * names the concrete type, and its result type already refuses one outside the range. This is what a
287
+ * CAST reaches, or data from a service binding: `text/html` under `image/*`, or `image/*` itself,
288
+ * would otherwise be served with a `Content-Type` the document does not permit. Type names compare
289
+ * case-insensitively (RFC 9110 section 8.3.1), and parameters such as `charset` are allowed.
290
+ */
291
+ export function mediaTypeWithin(served, range) {
292
+ const type = SERVED_MEDIA_TYPE.exec(served)?.[1];
293
+ if (type === undefined)
294
+ return false;
295
+ if (range === "*/*")
296
+ return true;
297
+ return range.endsWith("/*") && type.toLowerCase() === range.slice(0, -2).toLowerCase();
298
+ }