typespec-hono 0.21.0 → 0.23.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.
@@ -8,8 +8,8 @@ import { type EmitContext } from "@typespec/compiler";
8
8
  * forced it to be a `dependency` and made `--save-dev` fail at deploy and nowhere earlier. Emitting
9
9
  * the runtime removes the choice rather than documenting it.
10
10
  *
11
- * `typespec-hono/runtime` is still exported, for an application that points `runtime-module` at it
12
- * deliberately. It is no longer what the generated code reaches for by default.
11
+ * **Always this module.** `runtime-module` used to replace it, and that made an application own a
12
+ * copy of generated logic - see the `runtime-module-removed` diagnostic.
13
13
  */
14
- export declare const DEFAULT_RUNTIME_MODULE = "./runtime.gen.js";
14
+ export { DEFAULT_RUNTIME_MODULE } from "./app.js";
15
15
  export declare function $onEmit(context: EmitContext): Promise<void>;
@@ -1,10 +1,9 @@
1
- import { emitFile, resolvePath } from "@typespec/compiler";
1
+ import { emitFile, NoTarget, resolvePath } from "@typespec/compiler";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { emitHttpZod } from "typespec-http-zod";
5
5
  import { renderApp } from "./app.js";
6
6
  import { resolveBasePath } from "./base-path.js";
7
- import { securityFor } from "./security.js";
8
7
  import { reportDiagnostic } from "./lib.js";
9
8
  /**
10
9
  * This emitter's entry point. **the whole of `typespec-http-zod`, plus one file**.
@@ -40,27 +39,6 @@ function operationFor(emitted, verb, path) {
40
39
  function targetFor(emitted, verb, path) {
41
40
  return operationFor(emitted, verb, path)?.operation ?? emitted.service.namespace;
42
41
  }
43
- /**
44
- * What the generated files import their runtime contract from when the consumer sets nothing.
45
- *
46
- * **THIS package's runtime, not the library's, and the distinction is the whole of a defect that
47
- * shipped.** `app.gen.ts` names `AppEnv`, `Awaitable`, `Ctx`, `Result`, `RouteDeps` and
48
- * `selectContentType`; every one of them is declared in `src/runtime.ts` here. The library's runtime
49
- * exports `ResponseArm` and `armFor` and nothing else, and it is a TRANSITIVE dependency of a
50
- * consumer of this package, so under a strict `node_modules` its specifier does not resolve from
51
- * consumer code at all.
52
- *
53
- * Pointing at `typespec-hono/runtime` fixes both halves at once, because this module RE-EXPORTS
54
- * `ResponseArm` and `armFor` (see `runtime.ts`), which is what `schemas.gen.ts` imports. One
55
- * specifier, present in the consumer's own dependency, carrying every name both generated files
56
- * reference.
57
- *
58
- * **Measured in a fresh project installed from `pnpm pack` tarballs, because no test could see it:**
59
- * every compile in both harnesses sets `runtime-module` explicitly, so the default branch was ungraded
60
- * across 240 tests. `tsp compile` succeeded with zero diagnostics and `tsc` then reported two
61
- * `TS2307`s (`Cannot find module 'typespec-http-zod/runtime'`) one in each generated file.
62
- * `test/adopter.test.ts` is the arm that now opens that branch.
63
- */
64
42
  /** The header the emitted runtime carries, matching every other generated file. */
65
43
  function generatedRuntimeBanner(hint) {
66
44
  const second = hint === undefined
@@ -68,7 +46,6 @@ function generatedRuntimeBanner(hint) {
68
46
  : `// Regenerate with: ${hint}`;
69
47
  return `// GENERATED by typespec-hono. DO NOT EDIT.
70
48
  ${second}
71
- // Point \`runtime-module\` at a module of your own to replace it.
72
49
  `;
73
50
  }
74
51
  /**
@@ -80,27 +57,49 @@ ${second}
80
57
  * forced it to be a `dependency` and made `--save-dev` fail at deploy and nowhere earlier. Emitting
81
58
  * the runtime removes the choice rather than documenting it.
82
59
  *
83
- * `typespec-hono/runtime` is still exported, for an application that points `runtime-module` at it
84
- * deliberately. It is no longer what the generated code reaches for by default.
60
+ * **Always this module.** `runtime-module` used to replace it, and that made an application own a
61
+ * copy of generated logic - see the `runtime-module-removed` diagnostic.
85
62
  */
86
- export const DEFAULT_RUNTIME_MODULE = "./runtime.gen.js";
63
+ export { DEFAULT_RUNTIME_MODULE } from "./app.js";
87
64
  /** Where this package's own runtime source lives, to be copied beside the generated code. */
88
65
  const RUNTIME_SOURCE = fileURLToPath(new URL("../../src/runtime.ts", import.meta.url));
66
+ /**
67
+ * The options with `runtime-module` refused and removed, at the top level and per service.
68
+ *
69
+ * **Removed, not only reported**, because the library validates nothing about a key it no longer
70
+ * declares, and a wrapper passing options through should not hand it one.
71
+ */
72
+ function withoutRuntimeModule(context) {
73
+ const options = context.options;
74
+ const refuse = (value) => {
75
+ reportDiagnostic(context.program, {
76
+ code: "runtime-module-removed",
77
+ format: { value: String(value) },
78
+ target: NoTarget,
79
+ });
80
+ };
81
+ const { "runtime-module": topLevel, services, ...rest } = options;
82
+ if (topLevel !== undefined)
83
+ refuse(topLevel);
84
+ const kept = services === undefined
85
+ ? undefined
86
+ : Object.fromEntries(Object.entries(services).map(([name, service]) => {
87
+ const { "runtime-module": perService, ...others } = service;
88
+ if (perService !== undefined)
89
+ refuse(perService);
90
+ return [name, others];
91
+ }));
92
+ return {
93
+ ...context,
94
+ options: kept === undefined ? rest : { ...rest, services: kept },
95
+ };
96
+ }
89
97
  export async function $onEmit(context) {
90
- for (const emitted of await emitHttpZod(context, {
91
- defaultRuntimeModule: DEFAULT_RUNTIME_MODULE,
92
- })) {
93
- /**
94
- * Written only when the generated code actually points at it. An application that sets
95
- * `runtime-module` has supplied its own module and must not have a file it did not ask for
96
- * appear in its output directory.
97
- */
98
- if (emitted.options.runtimeModule === DEFAULT_RUNTIME_MODULE) {
99
- await emitFile(context.program, {
100
- path: resolvePath(emitted.outputDir, "runtime.gen.ts"),
101
- content: `${generatedRuntimeBanner(emitted.options.regenerateHint)}${readFileSync(RUNTIME_SOURCE, "utf8")}`,
102
- });
103
- }
98
+ for (const emitted of await emitHttpZod(withoutRuntimeModule(context))) {
99
+ await emitFile(context.program, {
100
+ path: resolvePath(emitted.outputDir, "runtime.gen.ts"),
101
+ content: `${generatedRuntimeBanner(emitted.options.regenerateHint)}${readFileSync(RUNTIME_SOURCE, "utf8")}`,
102
+ });
104
103
  /**
105
104
  * **The path the DOCUMENT says this service is served under.** An OpenAPI path is relative to
106
105
  * its server, so `@server("/api/v1")` plus `/accounts` publishes `/api/v1/accounts`. Mounting at
@@ -136,6 +135,17 @@ export async function $onEmit(context) {
136
135
  target: targetFor(emitted, route.verb, route.path),
137
136
  });
138
137
  },
138
+ unvalidatedResponseMediaType: (route, status, types) => {
139
+ reportDiagnostic(context.program, {
140
+ code: "unvalidated-response-media-type",
141
+ format: {
142
+ operationId: route.operationId,
143
+ status: String(status),
144
+ types: types.join(", "),
145
+ },
146
+ target: targetFor(emitted, route.verb, route.path),
147
+ });
148
+ },
139
149
  unvalidatableMediaType: (route, types) => {
140
150
  reportDiagnostic(context.program, {
141
151
  code: "unvalidatable-media-type",
@@ -143,10 +153,7 @@ export async function $onEmit(context) {
143
153
  target: targetFor(emitted, route.verb, route.path),
144
154
  });
145
155
  },
146
- }, base.basePaths, (verb, path) => {
147
- const operation = operationFor(emitted, verb, path);
148
- return operation === undefined ? [] : securityFor(context.program, operation);
149
- }),
156
+ }, base.basePaths),
150
157
  });
151
158
  }
152
159
  }
package/dist/src/lib.d.ts CHANGED
@@ -13,12 +13,14 @@ import { type EmitterOptions as HttpZodOptions } from "typespec-http-zod";
13
13
  * accepted and silently dropped, which produces output that is wrong in a way no test of either
14
14
  * package would see. `test/options.test.ts` asserts the forwarding as a CLASS.
15
15
  *
16
- * There is currently nothing to add: `runtime-module` belongs to the library, because the library is
17
- * what emits the annotated response arms that need it. This type exists as the seam rather than
18
- * because it carries anything today. The moment a Hono-only option appears it goes here, and the
19
- * derivation keeps the rest honest.
16
+ * **`runtime-module` is declared here only to be REFUSED** (`runtime-module-removed`). The library no
17
+ * longer has the option, and a schema without the key would report only "must NOT have additional
18
+ * properties", which tells a consumer upgrading from a version that documented the option neither why
19
+ * nor what to do instead.
20
20
  */
21
21
  export type EmitterOptions = HttpZodOptions & {
22
+ /** Refused. See `runtime-module-removed`. */
23
+ "runtime-module"?: string;
22
24
  /**
23
25
  * Per-service overrides this emitter adds on top of the library's.
24
26
  *
@@ -27,6 +29,7 @@ export type EmitterOptions = HttpZodOptions & {
27
29
  */
28
30
  services?: Record<string, {
29
31
  "emit-server"?: boolean;
32
+ "runtime-module"?: string;
30
33
  }>;
31
34
  };
32
35
  /**
@@ -54,12 +57,18 @@ declare const EmitterOptionsSchema: JSONSchemaType<EmitterOptions>;
54
57
  * exists only in the arrangement that BUILDS the package, and would ship silently.
55
58
  */
56
59
  type Diagnostics = {
60
+ "runtime-module-removed": {
61
+ readonly default: CallableMessage<["value"]>;
62
+ };
57
63
  "unsupported-path-template": {
58
64
  readonly default: CallableMessage<["template", "name"]>;
59
65
  };
60
66
  "unvalidatable-media-type": {
61
67
  readonly default: CallableMessage<["operationId", "types"]>;
62
68
  };
69
+ "unvalidated-response-media-type": {
70
+ readonly default: CallableMessage<["operationId", "status", "types"]>;
71
+ };
63
72
  };
64
73
  /**
65
74
  * **Annotated rather than inferred, and the reason is a packaging fact rather than a style
package/dist/src/lib.js CHANGED
@@ -8,9 +8,13 @@ import { EmitterOptionsSchema as httpZodOptions, } from "typespec-http-zod";
8
8
  *, never by copying the list.
9
9
  */
10
10
  const EmitterOptionsSchema = {
11
- ...httpZodOptions,
11
+ type: "object",
12
+ additionalProperties: false,
13
+ required: [],
12
14
  properties: {
13
15
  ...httpZodOptions.properties,
16
+ // Accepted by the schema so that setting it reaches `runtime-module-removed`.
17
+ "runtime-module": { type: "string", nullable: true },
14
18
  /**
15
19
  * **Spread from the library's own entry rather than restated**, so a per-service option added
16
20
  * there still validates here. Only `emit-server` is added.
@@ -23,6 +27,7 @@ const EmitterOptionsSchema = {
23
27
  properties: {
24
28
  ...httpZodOptions.properties.services.additionalProperties.properties,
25
29
  "emit-server": { type: "boolean", nullable: true },
30
+ "runtime-module": { type: "string", nullable: true },
26
31
  },
27
32
  required: [],
28
33
  },
@@ -73,6 +78,41 @@ const diagnostics = {
73
78
  default: paramMessage `'${"operationId"}' declares request media types this emitter cannot validate (${"types"}). Requests carrying them are refused rather than validated. Declare a media type with a parser -- JSON, multipart or urlencoded -- or handle the body yourself with '@body body: bytes'.`,
74
79
  },
75
80
  },
81
+ /**
82
+ * `runtime-module` is set, and this emitter no longer substitutes its runtime.
83
+ *
84
+ * **The option made an application OWN a copy of generated logic**, because the generated server
85
+ * imported the application's types from the same module as content negotiation, the HEAD guard and
86
+ * arm selection. So every consumer that needed its own caller context or result shape copied those
87
+ * too, and the copies aged: one gateway ran a runtime eleven releases behind the emitter, carrying a
88
+ * negotiation defect long since fixed.
89
+ *
90
+ * Nothing the option was for needs it now. The caller context is inferred from `deps.context`, the
91
+ * Hono environment is an interface an application augments, and what a handler returns is the union
92
+ * of the responses the document declares. **An error rather than a warning**, because the option's
93
+ * value is ignored, and an emitter that silently ignores a setting produces output its author did
94
+ * not ask for.
95
+ */
96
+ "runtime-module-removed": {
97
+ severity: "error",
98
+ messages: {
99
+ default: paramMessage `'runtime-module' is set to '${"value"}', and typespec-hono no longer substitutes its runtime: 'runtime.gen.ts' is always emitted. Remove the option and delete the module it pointed at. The caller context is inferred from 'deps.context', the Hono environment is augmented with 'declare module "./runtime.gen.js" { interface AppEnv { ... } }', and a handler returns '{ status, body, headers }' for any response the document declares. See the Upgrading section of docs/guides.md.`,
100
+ },
101
+ },
102
+ /**
103
+ * A response the handler supplies as text, served without being checked against its schema.
104
+ *
105
+ * A model declared under a media type that is not JSON - a `Pet` as `application/xml` - has no
106
+ * serialisation this emitter can derive from the document, so the handler returns the text and the
107
+ * route serves it as is. Reported for the same reason as its request-side twin: a route should not
108
+ * look validated when it is not.
109
+ */
110
+ "unvalidated-response-media-type": {
111
+ severity: "warning",
112
+ messages: {
113
+ default: paramMessage `'${"operationId"}' answers ${"status"} as ${"types"}, which this emitter cannot serialise from the schema. The handler returns that body as text and it is served without being validated. Declare a JSON media type, or a string body, to have it checked.`,
114
+ },
115
+ },
76
116
  "unsupported-path-template": {
77
117
  severity: "warning",
78
118
  messages: {
@@ -1,5 +1,5 @@
1
1
  import type { Context, Env, Input, MiddlewareHandler } from "hono";
2
- import type { ZodType } from "zod";
2
+ import type { output, ZodError, ZodType } from "zod";
3
3
  /**
4
4
  * One arm of an operation's declared response set, as the document publishes it.
5
5
  *
@@ -13,39 +13,17 @@ export interface ResponseArm {
13
13
  readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
14
14
  readonly schema: ZodType | undefined;
15
15
  /**
16
- * The media types this response offers, where the document names MORE than one.
17
- *
18
- * Absent where it names one, which is what an application already assumes, so "one type" and
19
- * "not carried" are the same state rather than two to tell apart. Present, it is the set to
20
- * negotiate against: `selectContentType` takes the caller's `Accept` and these.
16
+ * Every media type this response offers, including a single one. Absent where there is no body.
21
17
  */
22
18
  readonly contentTypes?: readonly string[];
23
19
  /**
24
- * The headers this response declares, as the document publishes them.
25
- *
26
- * **Two names, because two different things need them.** `name` is the WIRE name, which is what
27
- * the response sets; `property` is the name on the value the handler returned, which is where the
28
- * value is read from. `@header("x-correlation-id") correlationId: string` is `x-correlation-id`
29
- * on the wire and `correlationId` in the result, and they differ for any header with a hyphen.
30
- *
31
- * Absent where the response declares none.
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.
32
22
  */
33
23
  readonly headers?: readonly {
34
24
  readonly name: string;
35
- readonly property: string;
25
+ readonly optional: boolean;
36
26
  }[];
37
- readonly when?: {
38
- readonly property: string;
39
- /**
40
- * **A number too, because a `@statusCode` union selects by the status itself.**
41
- *
42
- * `model Created { @statusCode statusCode: 200 | 201 }` names the property that chooses, and
43
- * its values are the statuses. The discriminator case carries a boolean or string literal off
44
- * the body instead; both are the same question - which arm did the handler mean - so both use
45
- * this one field.
46
- */
47
- readonly value: boolean | number | string;
48
- };
49
27
  }
50
28
  /**
51
29
  * The arm that applies to a status, preferring an exact code, then its `NXX` range, then `default`.
@@ -78,30 +56,38 @@ export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
78
56
  * assertion invented to put the guarantee back.
79
57
  *
80
58
  * What is left for the app to supply is genuinely app-specific: how a request becomes a caller's
81
- * context, and how a result becomes a response. Everything else, routing, validation, which
82
- * validator applies to which target, what status each arm answers, is generated.
59
+ * context, and what a refusal looks like. Everything else, routing, validation, which validator
60
+ * applies to which target, which statuses an operation may answer with and how each one is served,
61
+ * is generated.
83
62
  */
84
63
  /**
85
- * How an operation's return value is wrapped.
64
+ * The Hono environment the generated server mounts on.
86
65
  *
87
- * Identity by default, so an operation may simply return its value. An app with a result envelope
88
- * points `runtime-module` at its own module and re-declares this as, say, `ServiceResult<T>`, which
89
- * is what keeps the generated `Operations` interface concretely typed end to end instead of falling
90
- * back to `unknown` and reintroducing the cast this whole change exists to delete.
91
- */
92
- export type Result<T> = T;
93
- /**
94
- * The Hono environment the generated server mounts on, and the caller context its operations take.
66
+ * **An interface, so an application AUGMENTS it rather than replacing this module.**
67
+ *
68
+ * ```ts
69
+ * declare module "./generated/runtime.gen.js" {
70
+ * interface AppEnv {
71
+ * Bindings: { BACKEND: Service<Backend> };
72
+ * Variables: { principal: Principal };
73
+ * }
74
+ * }
75
+ * ```
95
76
  *
96
- * **Concrete on purpose.** Making `registerRoutes` generic over the environment does not work:
97
- * Hono narrows `Context` per route and its conditional types cannot reduce
98
- * `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing the app
99
- * supplies is ever assignable and every call site needs a cast. Naming the types here instead, an
100
- * app points `runtime-module` at its own module and re-declares them, keeps every generated call
101
- * site concrete and cast-free. Identity defaults, so an app with neither can ignore both.
77
+ * That is the idiom Hono itself uses for `ContextVariableMap`, and it is what lets this module be
78
+ * emitted beside the generated code on every compile instead of being copied into an application and
79
+ * aged there. A copy was the only other way to name an environment: a gateway ran a runtime from
80
+ * `0.10.1` while the emitter reached `0.21.0`, carrying a content-negotiation defect fixed eleven
81
+ * releases earlier.
82
+ *
83
+ * **Concrete rather than a type parameter of `registerRoutes`, and that was measured twice.** Hono
84
+ * narrows `Context` per route, and its conditional types cannot reduce
85
+ * `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing an
86
+ * application supplies is ever assignable and every call site needs a cast. Confirmed again on hono
87
+ * 4.13.1 with TypeScript 7.0.2: three `TS2345`s on a three-route probe.
102
88
  */
103
- export type AppEnv = Env;
104
- export type Ctx = unknown;
89
+ export interface AppEnv extends Env {
90
+ }
105
91
  /** Anything an operation may hand back: the value, or a promise of it. */
106
92
  export type Awaitable<T> = T | Promise<T>;
107
93
  /**
@@ -157,20 +143,76 @@ export declare function selectContentType(accept: string | undefined, offered: r
157
143
  */
158
144
  export declare const headOnly: MiddlewareHandler;
159
145
  /**
160
- * **The request-body middleware used to live here, and it moved into `app.gen.ts`.**
146
+ * **A route whose template writes a query string, `/items?fixed=true{&param}`, is only that route when
147
+ * the request carries it.** A router matches paths, so the pairs are checked here, and a request
148
+ * without them gets the 404 any unrouted request gets, through whatever `app.notFound()` the
149
+ * application has set. In the runtime for the same reason as {@link headOnly}.
150
+ */
151
+ export declare const literalQuery: (pairs: readonly (readonly [string, string])[]) => MiddlewareHandler;
152
+ /**
153
+ * A response body that does not match the schema the document publishes for its status.
161
154
  *
162
- * `byContentType` and `optionalBody` were exported from this module and imported by the generated
163
- * server, which made them part of a SECOND contract this package has: what an application that
164
- * points `runtime-module` at a module of its own must export. That contract is easy to break without
165
- * noticing, and it was the reason a required single-media-type body kept `zValidator` - whose
166
- * `HTTPException` on an unreadable body is a `text/plain` 400 raised before `deps.invalid`, escaping
167
- * the app's error envelope. Routing those through `byContentType` closed the gap in one line and
168
- * made that export mandatory for every substituting app: measured, 15 arms red.
155
+ * **Thrown, so an application decides what a contract failure answers with in `app.onError`**,
156
+ * which is where Hono puts that decision. Before this existed every consumer made the same check in
157
+ * its own `respond` and answered differently - a 500, a 502, a 502 with the issues in the body, a 502
158
+ * with them redacted - and only one of them checked failure bodies at all.
169
159
  *
170
- * Emitting the middleware instead closes the gap and SHRINKS the runtime contract by these two
171
- * names. Nothing generated imports them, so keeping them here would leave a second implementation of
172
- * body reading that nothing exercises - which is how two copies of one rule drift apart.
160
+ * `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
161
+ * application that logs them should decide whether its responses may contain anything it would not
162
+ * log.
173
163
  */
164
+ export declare class ResponseContractError extends Error {
165
+ readonly operationId: string;
166
+ readonly status: number;
167
+ readonly issues: ZodError["issues"];
168
+ constructor(operationId: string, status: number, issues: ZodError["issues"]);
169
+ }
170
+ /**
171
+ * A handler answered with a status its operation does not declare.
172
+ *
173
+ * **Unreachable from a typed handler.** The generated `Operations` interface types every result as
174
+ * the union of the declared statuses, so a literal outside it does not compile. This is what a CAST
175
+ * produces, or a result that crossed a boundary the type system cannot see into, such as untyped
176
+ * data from a service binding. Thrown rather than served, because serving it would publish a status
177
+ * the contract does not state.
178
+ */
179
+ export declare class UndeclaredStatusError extends Error {
180
+ readonly operationId: string;
181
+ readonly result: unknown;
182
+ constructor(operationId: string, result: unknown);
183
+ }
184
+ /**
185
+ * The body a response SERVES: what the handler returned, parsed against the schema the document
186
+ * publishes for that status.
187
+ *
188
+ * **What is served is the PARSED value, not the one the handler returned.** A schema that strips
189
+ * undeclared keys therefore strips them from the wire too, which is how an internal field such as a
190
+ * tenant id is kept out of a response the document does not publish it in. Seven of the nine
191
+ * consumer surfaces surveyed relied on exactly that, each in its own hand-written `respond`.
192
+ *
193
+ * Synchronous, because nothing this emitter writes is asynchronous - `test/sync.test.ts` asserts it
194
+ * over the whole corpus - and the asynchronous path costs 2.6x per parse.
195
+ */
196
+ export declare function servedBody<S extends ZodType>(schema: S, value: unknown, operationId: string, status: number): output<S>;
197
+ /**
198
+ * Declared response headers, as the strings a response carries.
199
+ *
200
+ * An optional header the handler did not supply is omitted rather than sent as `"undefined"`, and a
201
+ * typed value - a `retry-after` declared `int32` - is written as its text. Hono's `HeaderRecord`
202
+ * refuses `undefined`, so dropping it here is also what keeps the generated call sites free of a
203
+ * conditional per header.
204
+ */
205
+ export declare function headersOf(declared: Readonly<Record<string, unknown>>): Record<string, string>;
206
+ /**
207
+ * Whether a media type a handler answers with lies inside a range the document offers.
208
+ *
209
+ * **A range is not a type a response can be sent as**, so where a status offers `image/*` the handler
210
+ * names the concrete type, and its result type already refuses one outside the range. This is what a
211
+ * CAST reaches, or data from a service binding: `text/html` under `image/*`, or `image/*` itself,
212
+ * would otherwise be served with a `Content-Type` the document does not permit. Type names compare
213
+ * case-insensitively (RFC 9110 section 8.3.1), and parameters such as `charset` are allowed.
214
+ */
215
+ export declare function mediaTypeWithin(served: string, range: string): boolean;
174
216
  /**
175
217
  * What the app provides. One object, passed once, rather than a module the generated file imports by
176
218
  * path. A generated server that hard-codes `../../backend.js` is only usable by the project it was
@@ -181,26 +223,25 @@ export declare const headOnly: MiddlewareHandler;
181
223
  * so a hook typed against a single `Context<E>` is not assignable at any real call site. Making the
182
224
  * hooks generic lets the app write functions that ignore both, without a cast anywhere.
183
225
  *
184
- * **`E` and `C` are PARAMETERS, and they have to be.** The defaults keep the bare `RouteDeps` the
185
- * generated server writes working for an app that substitutes nothing. An app that substitutes
186
- * anything binds them once, `export type RouteDeps = BaseRouteDeps<AppEnv, Ctx>` in the module it
187
- * points `runtime-module` at, and every hook is then typed against its own environment and its own
188
- * caller context.
226
+ * **`C` is the caller context, and `registerRoutes` INFERS it from `context`.** An application that
227
+ * returns a `Caller` from `context` has handlers typed `(ctx: Caller, input)`, with nothing to
228
+ * declare. It used to be a type an application re-declared in a substituted copy of this module.
189
229
  *
190
- * Re-exporting this interface unparameterised instead does not work, and the reason is not obvious:
191
- * **Hono's `Context` is INVARIANT in its environment**, because `Context.set` takes `E` as an
192
- * argument. So `Context<AppEnv, ...>` is not assignable to `Context<Env, ...>` however plain the
193
- * substituted environment is, and every generated `deps.*` call site fails. Separately, `context`
194
- * would keep returning the identity `Ctx` (`unknown`) which the app's own handlers then reject.
195
- * Measured before this was parameterised: **19 errors on a four-operation service.**
230
+ * **Nothing here renders a successful response, and that is the point.** Which statuses an operation
231
+ * may answer with, which body each one carries and how it is serialised are all things the document
232
+ * states, so the generated route does them. A `respond` hook used to be handed every arm and the
233
+ * handler's result, and every application re-implemented the choice by hand: five status-mapping
234
+ * tables across the consumers surveyed, none consulting the arms, one sending a declared 404 as 400.
235
+ * What remains are the refusals that happen before a handler runs, whose envelope only the
236
+ * application knows.
196
237
  */
197
- export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
238
+ export interface RouteDeps<E extends Env = AppEnv, C = unknown> {
198
239
  /**
199
240
  * The gate the DOCUMENT publishes, as middleware.
200
241
  *
201
242
  * **Which scopes an operation demands is a contract fact; how a token is verified is not.**
202
243
  * `@useAuth(OAuth2Auth<...>)` reaches OpenAPI as `security` per operation, so the requirement is
203
- * generated and this implements the check. The same split as `context` and `respond`. Emitted
244
+ * generated and this implements the check. The same split as `context`. Emitted
204
245
  * only where the operation declares scopes, which is why an internal surface with none is
205
246
  * unaffected.
206
247
  *
@@ -222,9 +263,17 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
222
263
  /**
223
264
  * The caller's context, or `null` when there is none to establish.
224
265
  *
225
- * `authentication` is what the DOCUMENT says, and only that: `"none"` where the operation
226
- * declares `@useAuth(NoAuth)` (`security: []` in OpenAPI) and `"required"` otherwise. Deciding
227
- * it at generation time is the point: the gate the document publishes is the gate that runs.
266
+ * `authentication` is what the DOCUMENT says, and only that:
267
+ *
268
+ * - `"none"`: no requirement asks for anything (`@useAuth(NoAuth)`, or no authentication at all);
269
+ * - `"optional"`: an anonymous alternative sits beside a real one (`NoAuth | BearerAuth`), so
270
+ * `authorize` has admitted this caller either way and a presented credential should still be
271
+ * read;
272
+ * - `"required"`: every alternative asks for something.
273
+ *
274
+ * **`"optional"` is new, and without it the middle case was reported as `"none"`**, so a caller
275
+ * with a valid token on an optional route was never established as a caller. Deciding it at
276
+ * generation time is the point: the gate the document publishes is the gate that runs.
228
277
  *
229
278
  * **It used to be `"none" | "account" | "resource"`, and the last two were an invention.** They
230
279
  * were chosen by whether the path had parameters, which no OpenAPI keyword expresses and which
@@ -232,7 +281,7 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
232
281
  * nothing published is the defect class this emitter exists to remove, so it is gone. An app that
233
282
  * needs the distinction can read the request, which is the one thing it definitely has.
234
283
  */
235
- readonly context: <P extends string, I extends Input>(c: Context<E, P, I>, authentication: "none" | "required") => C | null;
284
+ readonly context: <P extends string, I extends Input>(c: Context<E, P, I>, authentication: "none" | "optional" | "required") => C | null;
236
285
  /** The response when `context` returns `null`. */
237
286
  readonly noContext: <P extends string, I extends Input>(c: Context<E, P, I>) => Response;
238
287
  /**
@@ -253,9 +302,4 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
253
302
  readonly invalid: <P extends string, I extends Input>(result: {
254
303
  readonly success: boolean;
255
304
  }, c: Context<E, P, I>) => Response | undefined;
256
- /**
257
- * Turn an operation's result into a response, checked against the schema the document publishes
258
- * for the arm that applies. A bodyless success is an arm whose `schema` is `undefined`.
259
- */
260
- readonly respond: <P extends string, I extends Input>(c: Context<E, P, I>, arms: readonly ResponseArm[], result: unknown) => Awaitable<Response>;
261
305
  }