typespec-hono 0.22.0 → 0.24.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/src/runtime.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
  /**
5
5
  * One arm of an operation's declared response set, as the document publishes it.
@@ -14,36 +14,14 @@ export interface ResponseArm {
14
14
  readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
15
15
  readonly schema: ZodType | undefined;
16
16
  /**
17
- * The media types this response offers, where the document names MORE than one.
18
- *
19
- * Absent where it names one, which is what an application already assumes, so "one type" and
20
- * "not carried" are the same state rather than two to tell apart. Present, it is the set to
21
- * negotiate against: `selectContentType` takes the caller's `Accept` and these.
17
+ * Every media type this response offers, including a single one. Absent where there is no body.
22
18
  */
23
19
  readonly contentTypes?: readonly string[];
24
20
  /**
25
- * The headers this response declares, as the document publishes them.
26
- *
27
- * **Two names, because two different things need them.** `name` is the WIRE name, which is what
28
- * the response sets; `property` is the name on the value the handler returned, which is where the
29
- * value is read from. `@header("x-correlation-id") correlationId: string` is `x-correlation-id`
30
- * on the wire and `correlationId` in the result, and they differ for any header with a hyphen.
31
- *
32
- * Absent where the response declares none.
21
+ * The headers this response declares, by the WIRE name the response sets. `optional` is the
22
+ * document's `required: false`. Absent where the response declares none.
33
23
  */
34
- readonly headers?: readonly { readonly name: string; readonly property: string }[];
35
- readonly when?: {
36
- readonly property: string;
37
- /**
38
- * **A number too, because a `@statusCode` union selects by the status itself.**
39
- *
40
- * `model Created { @statusCode statusCode: 200 | 201 }` names the property that chooses, and
41
- * its values are the statuses. The discriminator case carries a boolean or string literal off
42
- * the body instead; both are the same question - which arm did the handler mean - so both use
43
- * this one field.
44
- */
45
- readonly value: boolean | number | string;
46
- };
24
+ readonly headers?: readonly { readonly name: string; readonly optional: boolean }[];
47
25
  }
48
26
 
49
27
  /**
@@ -85,32 +63,39 @@ export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
85
63
  * assertion invented to put the guarantee back.
86
64
  *
87
65
  * What is left for the app to supply is genuinely app-specific: how a request becomes a caller's
88
- * context, and how a result becomes a response. Everything else, routing, validation, which
89
- * validator applies to which target, what status each arm answers, is generated.
66
+ * context, and what a refusal looks like. Everything else, routing, validation, which validator
67
+ * applies to which target, which statuses an operation may answer with and how each one is served,
68
+ * is generated.
90
69
  */
91
70
 
92
71
  /**
93
- * How an operation's return value is wrapped.
72
+ * The Hono environment the generated server mounts on.
94
73
  *
95
- * Identity by default, so an operation may simply return its value. An app with a result envelope
96
- * points `runtime-module` at its own module and re-declares this as, say, `ServiceResult<T>`, which
97
- * is what keeps the generated `Operations` interface concretely typed end to end instead of falling
98
- * back to `unknown` and reintroducing the cast this whole change exists to delete.
99
- */
100
- export type Result<T> = T;
101
-
102
- /**
103
- * The Hono environment the generated server mounts on, and the caller context its operations take.
74
+ * **An interface, so an application AUGMENTS it rather than replacing this module.**
75
+ *
76
+ * ```ts
77
+ * declare module "./generated/runtime.gen.js" {
78
+ * interface AppEnv {
79
+ * Bindings: { BACKEND: Service<Backend> };
80
+ * Variables: { principal: Principal };
81
+ * }
82
+ * }
83
+ * ```
104
84
  *
105
- * **Concrete on purpose.** Making `registerRoutes` generic over the environment does not work:
106
- * Hono narrows `Context` per route and its conditional types cannot reduce
107
- * `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing the app
108
- * supplies is ever assignable and every call site needs a cast. Naming the types here instead, an
109
- * app points `runtime-module` at its own module and re-declares them, keeps every generated call
110
- * site concrete and cast-free. Identity defaults, so an app with neither can ignore both.
85
+ * That is the idiom Hono itself uses for `ContextVariableMap`, and it is what lets this module be
86
+ * emitted beside the generated code on every compile instead of being copied into an application and
87
+ * aged there. A copy was the only other way to name an environment: a gateway ran a runtime from
88
+ * `0.10.1` while the emitter reached `0.21.0`, carrying a content-negotiation defect fixed eleven
89
+ * releases earlier.
90
+ *
91
+ * **Concrete rather than a type parameter of `registerRoutes`, and that was measured twice.** Hono
92
+ * narrows `Context` per route, and its conditional types cannot reduce
93
+ * `IfAnyThenEmptyObject<E extends Env ? ...>` while `E` is an unbound parameter, so nothing an
94
+ * application supplies is ever assignable and every call site needs a cast. Confirmed again on hono
95
+ * 4.13.1 with TypeScript 7.0.2: three `TS2345`s on a three-route probe.
111
96
  */
112
- export type AppEnv = Env;
113
- export type Ctx = unknown;
97
+ // oxlint-disable-next-line typescript/no-empty-interface -- augmented by the application.
98
+ export interface AppEnv extends Env {}
114
99
 
115
100
  /** Anything an operation may hand back: the value, or a promise of it. */
116
101
  export type Awaitable<T> = T | Promise<T>;
@@ -140,7 +125,7 @@ export type Awaitable<T> = T | Promise<T>;
140
125
  * **Specificity was implemented as a tie-break and that was wrong three ways at once**, all of them
141
126
  * live in a published runtime until `test/negotiation.test.ts` was written. Scoring every matching
142
127
  * range and keeping the best `(q, specificity)` pair lets a permissive wildcard out-vote the precise
143
- * rule a caller wrote about that exact type - so `Accept: *​/*, application/json;q=0` was served
128
+ * rule a caller wrote about that exact type - so an `Accept` of any type at all followed by `application/json;q=0` was served
144
129
  * JSON, which is the one outcome an explicit refusal must never produce. The prose above stated the
145
130
  * right rules the whole time; nothing compared it to the code.
146
131
  *
@@ -214,20 +199,169 @@ export const headOnly: MiddlewareHandler = async (c, next) =>
214
199
  c.req.method === "HEAD" ? next() : c.notFound();
215
200
 
216
201
  /**
217
- * **The request-body middleware used to live here, and it moved into `app.gen.ts`.**
202
+ * **A route whose template writes a query string, `/items?fixed=true{&param}`, is only that route when
203
+ * the request carries it.** A router matches paths, so the pairs are checked here, and a request
204
+ * without them gets the 404 any unrouted request gets, through whatever `app.notFound()` the
205
+ * application has set. In the runtime for the same reason as {@link headOnly}.
206
+ */
207
+ export const literalQuery =
208
+ (pairs: readonly (readonly [string, string])[]): MiddlewareHandler =>
209
+ async (c, next) =>
210
+ pairs.every(([name, value]) => c.req.query(name) === value) ? next() : c.notFound();
211
+
212
+ /**
213
+ * A response body that does not match the schema the document publishes for its status.
214
+ *
215
+ * **Thrown, so an application decides what a contract failure answers with in `app.onError`**,
216
+ * which is where Hono puts that decision. Before this existed every consumer made the same check in
217
+ * its own `respond` and answered differently - a 500, a 502, a 502 with the issues in the body, a 502
218
+ * with them redacted - and only one of them checked failure bodies at all.
219
+ *
220
+ * `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
221
+ * application that logs them should decide whether its responses may contain anything it would not
222
+ * log.
223
+ */
224
+ export class ResponseContractError extends Error {
225
+ constructor(
226
+ readonly operationId: string,
227
+ readonly status: number,
228
+ readonly issues: ZodError["issues"],
229
+ /**
230
+ * Which half of the response broke its contract. **Defaulted, so the three-argument form an
231
+ * application may already construct or match on keeps working.**
232
+ */
233
+ readonly part: "body" | "headers" = "body",
234
+ ) {
235
+ super(
236
+ `${operationId} answered ${status} with ${part === "body" ? "a body" : "headers"} its document does not permit`,
237
+ );
238
+ this.name = "ResponseContractError";
239
+ }
240
+ }
241
+
242
+ /**
243
+ * A handler answered with a status its operation does not declare.
244
+ *
245
+ * **Unreachable from a typed handler.** The generated `Operations` interface types every result as
246
+ * the union of the declared statuses, so a literal outside it does not compile. This is what a CAST
247
+ * produces, or a result that crossed a boundary the type system cannot see into, such as untyped
248
+ * data from a service binding. Thrown rather than served, because serving it would publish a status
249
+ * the contract does not state.
250
+ */
251
+ export class UndeclaredStatusError extends Error {
252
+ constructor(
253
+ readonly operationId: string,
254
+ readonly result: unknown,
255
+ ) {
256
+ const status =
257
+ typeof result === "object" && result !== null && "status" in result
258
+ ? String(result.status)
259
+ : "no status";
260
+ super(`${operationId} answered ${status}, which its document does not declare`);
261
+ this.name = "UndeclaredStatusError";
262
+ }
263
+ }
264
+
265
+ /**
266
+ * The body a response SERVES: what the handler returned, parsed against the schema the document
267
+ * publishes for that status.
218
268
  *
219
- * `byContentType` and `optionalBody` were exported from this module and imported by the generated
220
- * server, which made them part of a SECOND contract this package has: what an application that
221
- * points `runtime-module` at a module of its own must export. That contract is easy to break without
222
- * noticing, and it was the reason a required single-media-type body kept `zValidator` - whose
223
- * `HTTPException` on an unreadable body is a `text/plain` 400 raised before `deps.invalid`, escaping
224
- * the app's error envelope. Routing those through `byContentType` closed the gap in one line and
225
- * made that export mandatory for every substituting app: measured, 15 arms red.
269
+ * **What is served is the PARSED value, not the one the handler returned.** A schema that strips
270
+ * undeclared keys therefore strips them from the wire too, which is how an internal field such as a
271
+ * tenant id is kept out of a response the document does not publish it in. Seven of the nine
272
+ * consumer surfaces surveyed relied on exactly that, each in its own hand-written `respond`.
226
273
  *
227
- * Emitting the middleware instead closes the gap and SHRINKS the runtime contract by these two
228
- * names. Nothing generated imports them, so keeping them here would leave a second implementation of
229
- * body reading that nothing exercises - which is how two copies of one rule drift apart.
274
+ * Synchronous, because nothing this emitter writes is asynchronous - `test/sync.test.ts` asserts it
275
+ * over the whole corpus - and the asynchronous path costs 2.6x per parse.
230
276
  */
277
+ export function servedBody<S extends ZodType>(
278
+ schema: S,
279
+ value: unknown,
280
+ operationId: string,
281
+ status: number,
282
+ ): output<S> {
283
+ const parsed = schema.safeParse(value);
284
+ if (!parsed.success) throw new ResponseContractError(operationId, status, parsed.error.issues);
285
+ return parsed.data;
286
+ }
287
+
288
+ /**
289
+ * Declared response headers, as the strings a response carries.
290
+ *
291
+ * An optional header the handler did not supply is omitted rather than sent as `"undefined"`, and a
292
+ * typed value - a `retry-after` declared `int32` - is written as its text. Hono's `HeaderRecord`
293
+ * refuses `undefined`, so dropping it here is also what keeps the generated call sites free of a
294
+ * conditional per header.
295
+ */
296
+ export function headersOf(declared: Readonly<Record<string, unknown>>): Record<string, string> {
297
+ const headers: Record<string, string> = {};
298
+ for (const [name, value] of Object.entries(declared)) {
299
+ if (value !== undefined) headers[name] = String(value);
300
+ }
301
+ return headers;
302
+ }
303
+
304
+ /**
305
+ * Declared response headers, CHECKED against the schemas the document publishes for them.
306
+ *
307
+ * **The body beside them was always checked and these never were.** `servedBody` parses every
308
+ * response body against its status's schema and throws `ResponseContractError`; a declared header
309
+ * went through `headersOf`, which is `String(value)` and nothing else. Measured on a generated
310
+ * server: a header the document constrains to `^[A-Za-z0-9-]+$` was served as
311
+ * `not a valid id"; injected=1` with a 200, while a body breaking its own schema on the same route
312
+ * threw. That asymmetry is the whole of what this removes.
313
+ *
314
+ * **Not a response-splitting hole, and worth saying so.** `new Response` refuses a header value
315
+ * containing CR or LF with a `TypeError` on both `workerd` and undici, so the runtime already stops
316
+ * the injection. What it does not stop is a response that disagrees with its own document, which a
317
+ * client generated from that document is entitled to rely on.
318
+ *
319
+ * **Undefined values are dropped BEFORE the check, not after.** An optional header the handler did
320
+ * not supply is absent rather than explicitly `undefined`, which is what the emitted schema's
321
+ * `.exactOptional()` means, and it is also what Hono's `HeaderRecord` requires. The check then runs
322
+ * on the values as the handler produced them - a `retry-after` declared `int32` is still a number
323
+ * here - because checking the text would check a different thing than the document describes.
324
+ *
325
+ * **What is returned is every header, including ones the schema does not mention.** A response
326
+ * carries headers the document does not declare, the `Content-Type` this emitter sets among them,
327
+ * and those are not a contract violation. The schema is a plain object schema, so it ignores them.
328
+ */
329
+ export function servedHeaders(
330
+ schema: ZodType,
331
+ declared: Readonly<Record<string, unknown>>,
332
+ operationId: string,
333
+ status: number,
334
+ ): Record<string, string> {
335
+ const present: Record<string, unknown> = {};
336
+ for (const [name, value] of Object.entries(declared)) {
337
+ if (value !== undefined) present[name] = value;
338
+ }
339
+ const parsed = schema.safeParse(present);
340
+ if (!parsed.success) {
341
+ throw new ResponseContractError(operationId, status, parsed.error.issues, "headers");
342
+ }
343
+ return headersOf(present);
344
+ }
345
+
346
+ /** RFC 9110 `token`, less `*`, which names a range rather than a type. */
347
+ const MEDIA_TOKEN = "[!#$%&'+.^_`|~0-9A-Za-z-]+";
348
+ const SERVED_MEDIA_TYPE = new RegExp(`^(${MEDIA_TOKEN})/${MEDIA_TOKEN}\\s*(;.*)?$`);
349
+
350
+ /**
351
+ * Whether a media type a handler answers with lies inside a range the document offers.
352
+ *
353
+ * **A range is not a type a response can be sent as**, so where a status offers `image/*` the handler
354
+ * names the concrete type, and its result type already refuses one outside the range. This is what a
355
+ * CAST reaches, or data from a service binding: `text/html` under `image/*`, or `image/*` itself,
356
+ * would otherwise be served with a `Content-Type` the document does not permit. Type names compare
357
+ * case-insensitively (RFC 9110 section 8.3.1), and parameters such as `charset` are allowed.
358
+ */
359
+ export function mediaTypeWithin(served: string, range: string): boolean {
360
+ const type = SERVED_MEDIA_TYPE.exec(served)?.[1];
361
+ if (type === undefined) return false;
362
+ if (range === "*/*") return true;
363
+ return range.endsWith("/*") && type.toLowerCase() === range.slice(0, -2).toLowerCase();
364
+ }
231
365
 
232
366
  /**
233
367
  * What the app provides. One object, passed once, rather than a module the generated file imports by
@@ -239,26 +373,25 @@ export const headOnly: MiddlewareHandler = async (c, next) =>
239
373
  * so a hook typed against a single `Context<E>` is not assignable at any real call site. Making the
240
374
  * hooks generic lets the app write functions that ignore both, without a cast anywhere.
241
375
  *
242
- * **`E` and `C` are PARAMETERS, and they have to be.** The defaults keep the bare `RouteDeps` the
243
- * generated server writes working for an app that substitutes nothing. An app that substitutes
244
- * anything binds them once, `export type RouteDeps = BaseRouteDeps<AppEnv, Ctx>` in the module it
245
- * points `runtime-module` at, and every hook is then typed against its own environment and its own
246
- * caller context.
376
+ * **`C` is the caller context, and `registerRoutes` INFERS it from `context`.** An application that
377
+ * returns a `Caller` from `context` has handlers typed `(ctx: Caller, input)`, with nothing to
378
+ * declare. It used to be a type an application re-declared in a substituted copy of this module.
247
379
  *
248
- * Re-exporting this interface unparameterised instead does not work, and the reason is not obvious:
249
- * **Hono's `Context` is INVARIANT in its environment**, because `Context.set` takes `E` as an
250
- * argument. So `Context<AppEnv, ...>` is not assignable to `Context<Env, ...>` however plain the
251
- * substituted environment is, and every generated `deps.*` call site fails. Separately, `context`
252
- * would keep returning the identity `Ctx` (`unknown`) which the app's own handlers then reject.
253
- * Measured before this was parameterised: **19 errors on a four-operation service.**
380
+ * **Nothing here renders a successful response, and that is the point.** Which statuses an operation
381
+ * may answer with, which body each one carries and how it is serialised are all things the document
382
+ * states, so the generated route does them. A `respond` hook used to be handed every arm and the
383
+ * handler's result, and every application re-implemented the choice by hand: five status-mapping
384
+ * tables across the consumers surveyed, none consulting the arms, one sending a declared 404 as 400.
385
+ * What remains are the refusals that happen before a handler runs, whose envelope only the
386
+ * application knows.
254
387
  */
255
- export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
388
+ export interface RouteDeps<E extends Env = AppEnv, C = unknown> {
256
389
  /**
257
390
  * The gate the DOCUMENT publishes, as middleware.
258
391
  *
259
392
  * **Which scopes an operation demands is a contract fact; how a token is verified is not.**
260
393
  * `@useAuth(OAuth2Auth<...>)` reaches OpenAPI as `security` per operation, so the requirement is
261
- * generated and this implements the check. The same split as `context` and `respond`. Emitted
394
+ * generated and this implements the check. The same split as `context`. Emitted
262
395
  * only where the operation declares scopes, which is why an internal surface with none is
263
396
  * unaffected.
264
397
  *
@@ -280,9 +413,17 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
280
413
  /**
281
414
  * The caller's context, or `null` when there is none to establish.
282
415
  *
283
- * `authentication` is what the DOCUMENT says, and only that: `"none"` where the operation
284
- * declares `@useAuth(NoAuth)` (`security: []` in OpenAPI) and `"required"` otherwise. Deciding
285
- * it at generation time is the point: the gate the document publishes is the gate that runs.
416
+ * `authentication` is what the DOCUMENT says, and only that:
417
+ *
418
+ * - `"none"`: no requirement asks for anything (`@useAuth(NoAuth)`, or no authentication at all);
419
+ * - `"optional"`: an anonymous alternative sits beside a real one (`NoAuth | BearerAuth`), so
420
+ * `authorize` has admitted this caller either way and a presented credential should still be
421
+ * read;
422
+ * - `"required"`: every alternative asks for something.
423
+ *
424
+ * **`"optional"` is new, and without it the middle case was reported as `"none"`**, so a caller
425
+ * with a valid token on an optional route was never established as a caller. Deciding it at
426
+ * generation time is the point: the gate the document publishes is the gate that runs.
286
427
  *
287
428
  * **It used to be `"none" | "account" | "resource"`, and the last two were an invention.** They
288
429
  * were chosen by whether the path had parameters, which no OpenAPI keyword expresses and which
@@ -292,7 +433,7 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
292
433
  */
293
434
  readonly context: <P extends string, I extends Input>(
294
435
  c: Context<E, P, I>,
295
- authentication: "none" | "required",
436
+ authentication: "none" | "optional" | "required",
296
437
  ) => C | null;
297
438
  /** The response when `context` returns `null`. */
298
439
  readonly noContext: <P extends string, I extends Input>(c: Context<E, P, I>) => Response;
@@ -318,13 +459,4 @@ export interface RouteDeps<E extends Env = AppEnv, C = Ctx> {
318
459
  result: { readonly success: boolean },
319
460
  c: Context<E, P, I>,
320
461
  ) => Response | undefined;
321
- /**
322
- * Turn an operation's result into a response, checked against the schema the document publishes
323
- * for the arm that applies. A bodyless success is an arm whose `schema` is `undefined`.
324
- */
325
- readonly respond: <P extends string, I extends Input>(
326
- c: Context<E, P, I>,
327
- arms: readonly ResponseArm[],
328
- result: unknown,
329
- ) => Awaitable<Response>;
330
462
  }
@@ -1,29 +0,0 @@
1
- import { type HttpOperation } from "@typespec/http";
2
- import type { Program } from "@typespec/compiler";
3
- import type { SecurityRequirement } from "./runtime.js";
4
- /**
5
- * What the DOCUMENT says a caller must satisfy, in the shape the document says it.
6
- *
7
- * **The scheme was being thrown away, and only "is a caller needed" survived.** `@useAuth(BearerAuth)`
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, so for
10
- * bearer, api-key and basic, which is the common case, nothing carried which scheme at all. An
11
- * application whose `context` read a cookie would happily serve a route the document says needs a
12
- * bearer token, and nothing anywhere would notice.
13
- *
14
- * **Passed through, never enforced here.** Which credentials satisfy a scheme is the application's
15
- * business and could not be anything else; which schemes an operation ACCEPTS is a contract fact and
16
- * is now generated. That is the same split as `context` and `respond`, applied to the half that was
17
- * missing.
18
- */
19
- export type { SecurityRequirement } from "./runtime.js";
20
- /**
21
- * The requirements an operation declares. Satisfying **any one** of them authorises the caller,
22
- * which is what an array of `security` objects means in OpenAPI, and why this is a list of lists
23
- * rather than a flat set of scopes.
24
- *
25
- * Empty when the operation declares `@useAuth(NoAuth)` or no authentication at all.
26
- */
27
- export declare function securityFor(program: Program, operation: HttpOperation): SecurityRequirement[];
28
- /** The requirements as a TypeScript literal, for the generated call site. */
29
- export declare function renderSecurity(requirements: readonly SecurityRequirement[]): string;
@@ -1,48 +0,0 @@
1
- import { getAuthenticationForOperation } from "@typespec/http";
2
- /**
3
- * The requirements an operation declares. Satisfying **any one** of them authorises the caller,
4
- * which is what an array of `security` objects means in OpenAPI, and why this is a list of lists
5
- * rather than a flat set of scopes.
6
- *
7
- * Empty when the operation declares `@useAuth(NoAuth)` or no authentication at all.
8
- */
9
- export function securityFor(program, operation) {
10
- const authentication = getAuthenticationForOperation(program, operation.operation);
11
- const requirements = [];
12
- for (const option of authentication?.options ?? []) {
13
- const requirement = {};
14
- let anonymous = false;
15
- for (const scheme of option.schemes) {
16
- /**
17
- * **`NoAuth` inside an option means that option needs nothing**, which is how a spec says
18
- * "authentication is optional here". It is not a scheme to demand, and emitting it as one
19
- * would refuse every anonymous caller the document permits.
20
- */
21
- if (scheme.type === "noAuth") {
22
- anonymous = true;
23
- continue;
24
- }
25
- /**
26
- * Scopes belong to the flows of an OAuth2 scheme; every other kind has none. Read from the
27
- * scheme rather than assumed, and de-duplicated because two flows may name the same scope.
28
- */
29
- const scopes = scheme.type === "oauth2"
30
- ? [...new Set(scheme.flows.flatMap((flow) => flow.scopes.map((scope) => scope.value)))]
31
- : [];
32
- requirement[scheme.id] = scopes;
33
- }
34
- if (anonymous && Object.keys(requirement).length === 0)
35
- continue;
36
- if (Object.keys(requirement).length > 0)
37
- requirements.push(requirement);
38
- }
39
- return requirements;
40
- }
41
- /** The requirements as a TypeScript literal, for the generated call site. */
42
- export function renderSecurity(requirements) {
43
- return `[${requirements
44
- .map((requirement) => `{ ${Object.entries(requirement)
45
- .map(([scheme, scopes]) => `${JSON.stringify(scheme)}: [${scopes.map((s) => JSON.stringify(s)).join(", ")}]`)
46
- .join(", ")} }`)
47
- .join(", ")}]`;
48
- }