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.
- package/README.md +85 -62
- package/dist/src/app.d.ts +63 -43
- package/dist/src/app.js +643 -231
- package/dist/src/emitter.d.ts +3 -3
- package/dist/src/emitter.js +52 -45
- package/dist/src/lib.d.ts +13 -4
- package/dist/src/lib.js +41 -1
- package/dist/src/runtime.d.ts +123 -79
- package/dist/src/runtime.js +107 -0
- package/package.json +12 -12
- package/src/runtime.ts +167 -84
- package/dist/src/security.d.ts +0 -29
- package/dist/src/security.js +0 -48
package/dist/src/emitter.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
12
|
-
*
|
|
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
|
|
14
|
+
export { DEFAULT_RUNTIME_MODULE } from "./app.js";
|
|
15
15
|
export declare function $onEmit(context: EmitContext): Promise<void>;
|
package/dist/src/emitter.js
CHANGED
|
@@ -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
|
-
* `
|
|
84
|
-
*
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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,
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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: {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
82
|
-
*
|
|
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
|
-
*
|
|
64
|
+
* The Hono environment the generated server mounts on.
|
|
86
65
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
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
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
|
104
|
-
|
|
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
|
-
* **
|
|
146
|
+
* **A route whose template writes a query string, `/items?fixed=true{¶m}`, 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
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
-
* **`
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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 =
|
|
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
|
|
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:
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
}
|