typespec-hono 0.1.0 → 0.2.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 +113 -265
- package/dist/src/app.d.ts +25 -19
- package/dist/src/app.js +238 -113
- package/dist/src/base-path.d.ts +18 -9
- package/dist/src/base-path.js +20 -18
- package/dist/src/emitter.d.ts +9 -18
- package/dist/src/emitter.js +51 -26
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lib.d.ts +13 -16
- package/dist/src/lib.js +28 -68
- package/dist/src/runtime.d.ts +102 -46
- package/dist/src/runtime.js +63 -17
- package/dist/src/security.d.ts +4 -4
- package/dist/src/security.js +2 -2
- package/dist/src/tsp-index.d.ts +1 -1
- package/dist/src/tsp-index.js +1 -1
- package/lib/main.tsp +1 -1
- package/package.json +6 -5
- package/src/runtime.ts +299 -0
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import type { Context, Env, Input, MiddlewareHandler } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* One arm of an operation's declared response set, as the document publishes it.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
+
* `test/runtime-parity.test.ts` 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
|
+
readonly when?: {
|
|
16
|
+
readonly property: string;
|
|
17
|
+
readonly value: boolean | string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The arm that applies to a status, preferring an exact code, then its `NXX` range, then `default`.
|
|
10
22
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* supply every name the generated files reference, and they reference these.
|
|
23
|
+
* That order is the document's own: OpenAPI resolves a response the same way, so an application
|
|
24
|
+
* choosing an arm by hand would have to re-derive this and could get it wrong differently.
|
|
14
25
|
*/
|
|
15
|
-
export
|
|
26
|
+
export declare function armFor(arms: readonly ResponseArm[], status: number): ResponseArm | undefined;
|
|
16
27
|
/**
|
|
17
28
|
* One acceptable combination of credentials, exactly as OpenAPI's `security` states it: scheme id to
|
|
18
29
|
* the scopes that scheme requires. Every entry in one requirement must be satisfied TOGETHER, and
|
|
19
30
|
* satisfying ANY ONE requirement authorises the caller.
|
|
20
31
|
*
|
|
21
|
-
*
|
|
22
|
-
* running server imports and must stay free of every build-time dependency
|
|
32
|
+
* **Declared here rather than beside the code that derives it**, because `./runtime` is what a
|
|
33
|
+
* running server imports and must stay free of every build-time dependency, a packaging arm asserts
|
|
23
34
|
* it names no `@typespec/*` package at all. An application should not drag a compiler into its
|
|
24
35
|
* Worker to read one type.
|
|
25
36
|
*/
|
|
@@ -27,8 +38,8 @@ export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
|
|
|
27
38
|
/**
|
|
28
39
|
* The contract between the GENERATED server and the app that mounts it.
|
|
29
40
|
*
|
|
30
|
-
*
|
|
31
|
-
* The emitter used to produce `GENERATED_ROUTES`
|
|
41
|
+
* **This exists because "here is a data table, write your own router" is not a deliverable.**
|
|
42
|
+
* The emitter used to produce `GENERATED_ROUTES` (one array of plain objects) and every consumer
|
|
32
43
|
* had to hand-write a loop that interpreted it at run time. In this repository that loop is 220
|
|
33
44
|
* lines, it sits outside every oracle the emitter is judged by, and it carries a cast that exists
|
|
34
45
|
* *only* because iterating a homogeneous array throws away the per-operation types the emitter knew:
|
|
@@ -37,14 +48,14 @@ export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
|
|
|
37
48
|
* assertion invented to put the guarantee back.
|
|
38
49
|
*
|
|
39
50
|
* What is left for the app to supply is genuinely app-specific: how a request becomes a caller's
|
|
40
|
-
* context, and how a result becomes a response. Everything else
|
|
41
|
-
* validator applies to which target, what status each arm answers
|
|
51
|
+
* context, and how a result becomes a response. Everything else, routing, validation, which
|
|
52
|
+
* validator applies to which target, what status each arm answers, is generated.
|
|
42
53
|
*/
|
|
43
54
|
/**
|
|
44
55
|
* How an operation's return value is wrapped.
|
|
45
56
|
*
|
|
46
57
|
* Identity by default, so an operation may simply return its value. An app with a result envelope
|
|
47
|
-
* points `runtime-module` at its own module and re-declares this as, say, `ServiceResult<T
|
|
58
|
+
* points `runtime-module` at its own module and re-declares this as, say, `ServiceResult<T>`, which
|
|
48
59
|
* is what keeps the generated `Operations` interface concretely typed end to end instead of falling
|
|
49
60
|
* back to `unknown` and reintroducing the cast this whole change exists to delete.
|
|
50
61
|
*/
|
|
@@ -52,11 +63,11 @@ export type Result<T> = T;
|
|
|
52
63
|
/**
|
|
53
64
|
* The Hono environment the generated server mounts on, and the caller context its operations take.
|
|
54
65
|
*
|
|
55
|
-
*
|
|
66
|
+
* **Concrete on purpose.** Making `registerRoutes` generic over the environment does not work:
|
|
56
67
|
* Hono narrows `Context` per route and its conditional types cannot reduce
|
|
57
|
-
* `IfAnyThenEmptyObject<E extends Env ?
|
|
58
|
-
* supplies is ever assignable and every call site needs a cast. Naming the types here instead
|
|
59
|
-
* app points `runtime-module` at its own module and re-declares them
|
|
68
|
+
* `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing the app
|
|
69
|
+
* supplies is ever assignable and every call site needs a cast. Naming the types here instead, an
|
|
70
|
+
* app points `runtime-module` at its own module and re-declares them, keeps every generated call
|
|
60
71
|
* site concrete and cast-free. Identity defaults, so an app with neither can ignore both.
|
|
61
72
|
*/
|
|
62
73
|
export type AppEnv = Env;
|
|
@@ -64,56 +75,101 @@ export type Ctx = unknown;
|
|
|
64
75
|
/** Anything an operation may hand back: the value, or a promise of it. */
|
|
65
76
|
export type Awaitable<T> = T | Promise<T>;
|
|
66
77
|
/**
|
|
67
|
-
* Pick the media type to serve, per RFC 9110
|
|
78
|
+
* Pick the media type to serve, per RFC 9110 section 12.5.1.
|
|
68
79
|
*
|
|
69
|
-
*
|
|
80
|
+
* **In the runtime rather than in {@link RouteDeps}, deliberately.** The test for admitting
|
|
70
81
|
* anything to `deps` is *the generated code cannot proceed without an answer*, and this is not that:
|
|
71
82
|
* which media types an operation offers is a contract fact the emitter reads from the document, and
|
|
72
83
|
* how `Accept` selects among them is specified by the RFC. Both sides are derivable, so an app that
|
|
73
|
-
* had to supply this would be re-implementing the standard
|
|
84
|
+
* had to supply this would be re-implementing the standard, and could get it wrong differently from
|
|
74
85
|
* everybody else.
|
|
75
86
|
*
|
|
76
87
|
* The rules that matter, and that a naive `includes()` gets wrong:
|
|
77
|
-
* - **absent or empty `Accept` means anything is acceptable
|
|
88
|
+
* - **absent or empty `Accept` means anything is acceptable**, serve the first offer;
|
|
78
89
|
* - **`q=0` is a REFUSAL**, not a weak preference, so a range scoring zero can never be chosen;
|
|
79
|
-
* - **specificity breaks ties before quality does**:
|
|
80
|
-
* equal `q
|
|
90
|
+
* - **specificity breaks ties before quality does**: an exact type beats a subtype wildcard
|
|
91
|
+
* (`text/*`), which beats the fully wildcard range, at equal `q`. That is why the score is a
|
|
92
|
+
* pair rather than a number. The fully wildcard range is not written literally here because it
|
|
93
|
+
* would close this comment;
|
|
81
94
|
* - parameters after the media range (`;charset=utf-8`) are not part of the match.
|
|
82
95
|
*
|
|
83
|
-
* Returns `undefined` when nothing offered is acceptable
|
|
96
|
+
* Returns `undefined` when nothing offered is acceptable. The caller answers 406, and the
|
|
84
97
|
* difference between "no preference" and "no acceptable option" is exactly what that turns on.
|
|
85
98
|
*/
|
|
86
99
|
export declare function selectContentType(accept: string | undefined, offered: readonly string[]): string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Let only a real HEAD request through.
|
|
102
|
+
*
|
|
103
|
+
* Hono rewrites HEAD to GET before matching, so a `@head` operation has to be registered under GET
|
|
104
|
+
* to be reachable at all. Where the document declares no GET on that path, this keeps the
|
|
105
|
+
* registration honest: a GET gets the 404 it would have got if the route had never been registered,
|
|
106
|
+
* and only a HEAD reaches the validators and the handler. `c.req.method` still reads `HEAD` after the
|
|
107
|
+
* rewrite, which is what makes the distinction possible.
|
|
108
|
+
*
|
|
109
|
+
* In the runtime rather than in {@link RouteDeps} on the usual test: the generated code can proceed
|
|
110
|
+
* without asking the app anything. Which verbs the document declares is a contract fact, and the
|
|
111
|
+
* answer for a verb it does not declare is the same 404 any unrouted request already gets, through
|
|
112
|
+
* whatever `app.notFound()` the application has set.
|
|
113
|
+
*
|
|
114
|
+
* A plain middleware rather than `except()` from `hono/combine`, because `except` wraps the final
|
|
115
|
+
* handler and erases its response type, and Hono's RPC client derives its whole surface from that
|
|
116
|
+
* type. Measured: `hc<typeof app>` resolved a wrapped route's body to `unknown`.
|
|
117
|
+
*/
|
|
118
|
+
export declare const headOnly: MiddlewareHandler;
|
|
119
|
+
/**
|
|
120
|
+
* Apply the validator that parses the media type the request actually carries.
|
|
121
|
+
*
|
|
122
|
+
* A route may declare several request media types needing different parsers -- `addPet` in the
|
|
123
|
+
* Swagger Petstore accepts JSON, XML and urlencoded on one path. `zValidator`'s target is fixed when
|
|
124
|
+
* the server is generated; which parser applies is decided by the caller's `Content-Type` when the
|
|
125
|
+
* request arrives. Those are different times, and only the second one has the answer.
|
|
126
|
+
*
|
|
127
|
+
* **Before this, one target was chosen for the whole route and everything else was rejected.** A
|
|
128
|
+
* form-encoded body to a route declaring JSON first was handed to `c.req.json()` and answered 400,
|
|
129
|
+
* with no diagnostic anywhere. The status looked like the caller's fault and was not.
|
|
130
|
+
*
|
|
131
|
+
* Parameters after the media type (`; charset=utf-8`, `; boundary=...`) are not part of the match,
|
|
132
|
+
* which matters because a multipart request always carries a boundary.
|
|
133
|
+
*
|
|
134
|
+
* A `Content-Type` matching nothing declared falls through to the first validator, which reproduces
|
|
135
|
+
* the previous behaviour exactly for that case: the body fails to parse and `deps.invalid` answers.
|
|
136
|
+
* No status is invented here that the document does not describe.
|
|
137
|
+
*/
|
|
138
|
+
export declare function byContentType<E extends Env>(validators: readonly (readonly [
|
|
139
|
+
mediaType: string,
|
|
140
|
+
target: string,
|
|
141
|
+
validator: MiddlewareHandler<E>
|
|
142
|
+
])[]): MiddlewareHandler<E>;
|
|
87
143
|
/**
|
|
88
144
|
* What the app provides. One object, passed once, rather than a module the generated file imports by
|
|
89
|
-
* path
|
|
145
|
+
* path. A generated server that hard-codes `../../backend.js` is only usable by the project it was
|
|
90
146
|
* generated in, and this one has to be usable by any.
|
|
91
147
|
*
|
|
92
|
-
*
|
|
93
|
-
* `Context` per route
|
|
148
|
+
* **The hooks are generic over Hono's path and input parameters, deliberately.** Hono narrows
|
|
149
|
+
* `Context` per route, by the literal path, and by whatever the validators on that route produced,
|
|
94
150
|
* so a hook typed against a single `Context<E>` is not assignable at any real call site. Making the
|
|
95
151
|
* hooks generic lets the app write functions that ignore both, without a cast anywhere.
|
|
96
152
|
*
|
|
97
|
-
*
|
|
153
|
+
* **`E` and `C` are PARAMETERS, and they have to be.** The defaults keep the bare `RouteDeps` the
|
|
98
154
|
* generated server writes working for an app that substitutes nothing. An app that substitutes
|
|
99
|
-
* anything binds them once
|
|
100
|
-
* points `runtime-module` at
|
|
155
|
+
* anything binds them once, `export type RouteDeps = BaseRouteDeps<AppEnv, Ctx>` in the module it
|
|
156
|
+
* points `runtime-module` at, and every hook is then typed against its own environment and its own
|
|
101
157
|
* caller context.
|
|
102
158
|
*
|
|
103
159
|
* Re-exporting this interface unparameterised instead does not work, and the reason is not obvious:
|
|
104
160
|
* **Hono's `Context` is INVARIANT in its environment**, because `Context.set` takes `E` as an
|
|
105
|
-
* argument. So `Context<AppEnv,
|
|
161
|
+
* argument. So `Context<AppEnv, ...>` is not assignable to `Context<Env, ...>` however plain the
|
|
106
162
|
* substituted environment is, and every generated `deps.*` call site fails. Separately, `context`
|
|
107
|
-
* would keep returning the identity `Ctx`
|
|
163
|
+
* would keep returning the identity `Ctx` (`unknown`) which the app's own handlers then reject.
|
|
108
164
|
* Measured before this was parameterised: **19 errors on a four-operation service.**
|
|
109
165
|
*/
|
|
110
166
|
export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
|
|
111
167
|
/**
|
|
112
168
|
* The gate the DOCUMENT publishes, as middleware.
|
|
113
169
|
*
|
|
114
|
-
*
|
|
115
|
-
* `@useAuth(OAuth2Auth
|
|
116
|
-
* generated and this implements the check
|
|
170
|
+
* **Which scopes an operation demands is a contract fact; how a token is verified is not.**
|
|
171
|
+
* `@useAuth(OAuth2Auth<...>)` reaches OpenAPI as `security` per operation, so the requirement is
|
|
172
|
+
* generated and this implements the check. The same split as `context` and `respond`. Emitted
|
|
117
173
|
* only where the operation declares scopes, which is why an internal surface with none is
|
|
118
174
|
* unaffected.
|
|
119
175
|
*
|
|
@@ -121,25 +177,25 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
|
|
|
121
177
|
* to scopes while the document published eleven, so a surface mounted with its gate silently
|
|
122
178
|
* dropped.
|
|
123
179
|
*
|
|
124
|
-
*
|
|
125
|
-
* half of the same defect.** `@useAuth(BearerAuth)` publishes `security: [{ "BearerAuth": [] }]`
|
|
126
|
-
* no scopes
|
|
180
|
+
* **It receives the document's REQUIREMENTS, not a flat list of scopes, and that is the second
|
|
181
|
+
* half of the same defect.** `@useAuth(BearerAuth)` publishes `security: [{ "BearerAuth": [] }]`
|
|
182
|
+
* with no scopes, so a scopes-only gate was emitted for OAuth2 and for nothing else. Bearer, api-key
|
|
127
183
|
* and basic, which is most services, carried no gate at all and rested entirely on `context`
|
|
128
184
|
* returning null. An app whose `context` read a cookie would serve a route the document says needs
|
|
129
185
|
* a bearer token.
|
|
130
186
|
*
|
|
131
187
|
* Satisfying ANY ONE requirement authorises the caller, and every scheme WITHIN a requirement must
|
|
132
|
-
* be satisfied together
|
|
188
|
+
* be satisfied together, which is exactly what an array of OpenAPI `security` objects means.
|
|
133
189
|
*/
|
|
134
190
|
readonly authorize: (requirements: readonly SecurityRequirement[]) => MiddlewareHandler<E>;
|
|
135
191
|
/**
|
|
136
192
|
* The caller's context, or `null` when there is none to establish.
|
|
137
193
|
*
|
|
138
194
|
* `authentication` is what the DOCUMENT says, and only that: `"none"` where the operation
|
|
139
|
-
* declares `@useAuth(NoAuth)`
|
|
195
|
+
* declares `@useAuth(NoAuth)` (`security: []` in OpenAPI) and `"required"` otherwise. Deciding
|
|
140
196
|
* it at generation time is the point: the gate the document publishes is the gate that runs.
|
|
141
197
|
*
|
|
142
|
-
*
|
|
198
|
+
* **It used to be `"none" | "account" | "resource"`, and the last two were an invention.** They
|
|
143
199
|
* were chosen by whether the path had parameters, which no OpenAPI keyword expresses and which
|
|
144
200
|
* merely happened to fit the first consumer. A generated server enforcing a rule derived from
|
|
145
201
|
* nothing published is the defect class this emitter exists to remove, so it is gone. An app that
|
|
@@ -149,7 +205,7 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
|
|
|
149
205
|
/** The response when `context` returns `null`. */
|
|
150
206
|
readonly noContext: <P extends string, I extends Input>(c: Context<E, P, I>) => Response;
|
|
151
207
|
/**
|
|
152
|
-
* The response when the caller's `Accept` matches nothing the operation offers
|
|
208
|
+
* The response when the caller's `Accept` matches nothing the operation offers, a 406.
|
|
153
209
|
*
|
|
154
210
|
* Emitted only on routes where the document declares more than one media type for a status, so
|
|
155
211
|
* a service without content negotiation never sees it. Same shape of hook as {@link noContext}
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The arm that applies to a status, preferring an exact code, then its `NXX` range, then `default`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the rule for reading an array containing `4XX` and `default` belongs beside it. A consumer serving
|
|
7
|
-
* those validators from Express needs both and should not depend on a Hono emitter to get them.
|
|
8
|
-
*
|
|
9
|
-
* Re-exported rather than merely available, so an application still has ONE runtime import and the
|
|
10
|
-
* `runtime-module` substitution keeps working unchanged: a module an app points that option at has to
|
|
11
|
-
* supply every name the generated files reference, and they reference these.
|
|
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.
|
|
12
6
|
*/
|
|
13
|
-
export
|
|
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
|
+
}
|
|
14
12
|
/**
|
|
15
|
-
* Pick the media type to serve, per RFC 9110
|
|
13
|
+
* Pick the media type to serve, per RFC 9110 section 12.5.1.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* **In the runtime rather than in {@link RouteDeps}, deliberately.** The test for admitting
|
|
18
16
|
* anything to `deps` is *the generated code cannot proceed without an answer*, and this is not that:
|
|
19
17
|
* which media types an operation offers is a contract fact the emitter reads from the document, and
|
|
20
18
|
* how `Accept` selects among them is specified by the RFC. Both sides are derivable, so an app that
|
|
21
|
-
* had to supply this would be re-implementing the standard
|
|
19
|
+
* had to supply this would be re-implementing the standard, and could get it wrong differently from
|
|
22
20
|
* everybody else.
|
|
23
21
|
*
|
|
24
22
|
* The rules that matter, and that a naive `includes()` gets wrong:
|
|
25
|
-
* - **absent or empty `Accept` means anything is acceptable
|
|
23
|
+
* - **absent or empty `Accept` means anything is acceptable**, serve the first offer;
|
|
26
24
|
* - **`q=0` is a REFUSAL**, not a weak preference, so a range scoring zero can never be chosen;
|
|
27
|
-
* - **specificity breaks ties before quality does**:
|
|
28
|
-
* equal `q
|
|
25
|
+
* - **specificity breaks ties before quality does**: an exact type beats a subtype wildcard
|
|
26
|
+
* (`text/*`), which beats the fully wildcard range, at equal `q`. That is why the score is a
|
|
27
|
+
* pair rather than a number. The fully wildcard range is not written literally here because it
|
|
28
|
+
* would close this comment;
|
|
29
29
|
* - parameters after the media range (`;charset=utf-8`) are not part of the match.
|
|
30
30
|
*
|
|
31
|
-
* Returns `undefined` when nothing offered is acceptable
|
|
31
|
+
* Returns `undefined` when nothing offered is acceptable. The caller answers 406, and the
|
|
32
32
|
* difference between "no preference" and "no acceptable option" is exactly what that turns on.
|
|
33
33
|
*/
|
|
34
34
|
export function selectContentType(accept, offered) {
|
|
@@ -61,3 +61,49 @@ export function selectContentType(accept, offered) {
|
|
|
61
61
|
}
|
|
62
62
|
return best?.type;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Let only a real HEAD request through.
|
|
66
|
+
*
|
|
67
|
+
* Hono rewrites HEAD to GET before matching, so a `@head` operation has to be registered under GET
|
|
68
|
+
* to be reachable at all. Where the document declares no GET on that path, this keeps the
|
|
69
|
+
* registration honest: a GET gets the 404 it would have got if the route had never been registered,
|
|
70
|
+
* and only a HEAD reaches the validators and the handler. `c.req.method` still reads `HEAD` after the
|
|
71
|
+
* rewrite, which is what makes the distinction possible.
|
|
72
|
+
*
|
|
73
|
+
* In the runtime rather than in {@link RouteDeps} on the usual test: the generated code can proceed
|
|
74
|
+
* without asking the app anything. Which verbs the document declares is a contract fact, and the
|
|
75
|
+
* answer for a verb it does not declare is the same 404 any unrouted request already gets, through
|
|
76
|
+
* whatever `app.notFound()` the application has set.
|
|
77
|
+
*
|
|
78
|
+
* A plain middleware rather than `except()` from `hono/combine`, because `except` wraps the final
|
|
79
|
+
* handler and erases its response type, and Hono's RPC client derives its whole surface from that
|
|
80
|
+
* type. Measured: `hc<typeof app>` resolved a wrapped route's body to `unknown`.
|
|
81
|
+
*/
|
|
82
|
+
export const headOnly = async (c, next) => c.req.method === "HEAD" ? next() : c.notFound();
|
|
83
|
+
/**
|
|
84
|
+
* Apply the validator that parses the media type the request actually carries.
|
|
85
|
+
*
|
|
86
|
+
* A route may declare several request media types needing different parsers -- `addPet` in the
|
|
87
|
+
* Swagger Petstore accepts JSON, XML and urlencoded on one path. `zValidator`'s target is fixed when
|
|
88
|
+
* the server is generated; which parser applies is decided by the caller's `Content-Type` when the
|
|
89
|
+
* request arrives. Those are different times, and only the second one has the answer.
|
|
90
|
+
*
|
|
91
|
+
* **Before this, one target was chosen for the whole route and everything else was rejected.** A
|
|
92
|
+
* form-encoded body to a route declaring JSON first was handed to `c.req.json()` and answered 400,
|
|
93
|
+
* with no diagnostic anywhere. The status looked like the caller's fault and was not.
|
|
94
|
+
*
|
|
95
|
+
* Parameters after the media type (`; charset=utf-8`, `; boundary=...`) are not part of the match,
|
|
96
|
+
* which matters because a multipart request always carries a boundary.
|
|
97
|
+
*
|
|
98
|
+
* A `Content-Type` matching nothing declared falls through to the first validator, which reproduces
|
|
99
|
+
* the previous behaviour exactly for that case: the body fails to parse and `deps.invalid` answers.
|
|
100
|
+
* No status is invented here that the document does not describe.
|
|
101
|
+
*/
|
|
102
|
+
export function byContentType(validators) {
|
|
103
|
+
return async (c, next) => {
|
|
104
|
+
const declared = (c.req.header("content-type") ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
105
|
+
const matched = validators.find(([mediaType]) => mediaType.toLowerCase() === declared);
|
|
106
|
+
const [, , validator] = matched ?? validators[0];
|
|
107
|
+
return validator(c, next);
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/src/security.d.ts
CHANGED
|
@@ -4,21 +4,21 @@ import type { SecurityRequirement } from "./runtime.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* What the DOCUMENT says a caller must satisfy, in the shape the document says it.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* **The scheme was being thrown away, and only "is a caller needed" survived.** `@useAuth(BearerAuth)`
|
|
8
8
|
* reaches OpenAPI as `security: [{ "BearerAuth": [] }]`, and this emitter reduced that to
|
|
9
|
-
* `deps.context(c, "required")`. A gate was emitted ONLY when the scheme carried scopes
|
|
9
|
+
* `deps.context(c, "required")`. A gate was emitted ONLY when the scheme carried scopes, so for
|
|
10
10
|
* bearer, api-key and basic, which is the common case, nothing carried which scheme at all. An
|
|
11
11
|
* application whose `context` read a cookie would happily serve a route the document says needs a
|
|
12
12
|
* bearer token, and nothing anywhere would notice.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* **Passed through, never enforced here.** Which credentials satisfy a scheme is the application's
|
|
15
15
|
* business and could not be anything else; which schemes an operation ACCEPTS is a contract fact and
|
|
16
16
|
* is now generated. That is the same split as `context` and `respond`, applied to the half that was
|
|
17
17
|
* missing.
|
|
18
18
|
*/
|
|
19
19
|
export type { SecurityRequirement } from "./runtime.js";
|
|
20
20
|
/**
|
|
21
|
-
* The requirements an operation declares. Satisfying **any one** of them authorises the caller
|
|
21
|
+
* The requirements an operation declares. Satisfying **any one** of them authorises the caller,
|
|
22
22
|
* which is what an array of `security` objects means in OpenAPI, and why this is a list of lists
|
|
23
23
|
* rather than a flat set of scopes.
|
|
24
24
|
*
|
package/dist/src/security.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAuthenticationForOperation } from "@typespec/http";
|
|
2
2
|
/**
|
|
3
|
-
* The requirements an operation declares. Satisfying **any one** of them authorises the caller
|
|
3
|
+
* The requirements an operation declares. Satisfying **any one** of them authorises the caller,
|
|
4
4
|
* which is what an array of `security` objects means in OpenAPI, and why this is a list of lists
|
|
5
5
|
* rather than a flat set of scopes.
|
|
6
6
|
*
|
|
@@ -14,7 +14,7 @@ export function securityFor(program, operation) {
|
|
|
14
14
|
let anonymous = false;
|
|
15
15
|
for (const scheme of option.schemes) {
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* **`NoAuth` inside an option means that option needs nothing**, which is how a spec says
|
|
18
18
|
* "authentication is optional here". It is not a scheme to demand, and emitting it as one
|
|
19
19
|
* would refuse every anonymous caller the document permits.
|
|
20
20
|
*/
|
package/dist/src/tsp-index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What TypeSpec's compiler loads for this library.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* **There is deliberately no `$decorators` export**, here or in `typespec-http-zod`. Four once
|
|
5
5
|
* existed and every one let a spec state something `@typespec/openapi3` could not publish, so the
|
|
6
6
|
* emitted validator enforced a rule no caller reading the contract could see. A decorator is not a
|
|
7
7
|
* convenience; it is a second contract.
|
package/dist/src/tsp-index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What TypeSpec's compiler loads for this library.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* **There is deliberately no `$decorators` export**, here or in `typespec-http-zod`. Four once
|
|
5
5
|
* existed and every one let a spec state something `@typespec/openapi3` could not publish, so the
|
|
6
6
|
* emitted validator enforced a rule no caller reading the contract could see. A decorator is not a
|
|
7
7
|
* convenience; it is a second contract.
|
package/lib/main.tsp
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// The emitter library itself
|
|
1
|
+
// The emitter library itself. One diagnostic and its options schema. There are no decorators; see
|
|
2
2
|
// `src/tsp-index.ts` for why, and `typespec-http-zod` for the rest of the options this accepts.
|
|
3
3
|
import "../dist/src/tsp-index.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-hono",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition, agreeing with the OpenAPI document @typespec/openapi3 publishes from the same source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
7
7
|
"codegen",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"url": "git+https://github.com/bison-digital/typespec-hono.git"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"
|
|
24
|
+
"!dist/test/**",
|
|
25
25
|
"dist/**",
|
|
26
|
-
"
|
|
26
|
+
"lib/*.tsp",
|
|
27
|
+
"src/runtime.ts"
|
|
27
28
|
],
|
|
28
29
|
"type": "module",
|
|
29
30
|
"main": "dist/src/index.js",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"provenance": true
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"typespec-http-zod": "^0.
|
|
47
|
+
"typespec-http-zod": "^0.2.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@hono/zod-openapi": "^1.4.0",
|