@tetsujs/openapi 0.4.1 → 0.5.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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * JSON Schema 2020-12, as written by hand.
3
+ *
4
+ * Every other schema in a document comes from a validator, which describes
5
+ * itself. What a hook adds to its answer — a field next to the envelope, a
6
+ * header — has no validator behind it, and is written as JSON Schema
7
+ * directly. Typed closed, keyword by keyword, so that an editor offers the
8
+ * keywords and a misspelled one is an error: an open record would take
9
+ * `minimun` and `{ type: "int" }` and put them in the document as they are.
10
+ *
11
+ * Closed on purpose, not by omission. The generator asks every schema for
12
+ * the 2020-12 dialect, and a dialect's keywords are a fixed list. The one
13
+ * open door is what OpenAPI itself allows in a schema — extensions, named
14
+ * `x-` — and no misspelling of a keyword starts with it.
15
+ *
16
+ * @module
17
+ */
18
+ /** A primitive type name of JSON Schema. */
19
+ export type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
20
+ /**
21
+ * A schema: an object of keywords, or `true` and `false` — the schemas
22
+ * everything and nothing satisfy.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const retryAfter: JsonSchema = { type: "integer", minimum: 0 };
27
+ * ```
28
+ */
29
+ export type JsonSchema = JsonSchemaKeywords | boolean;
30
+ /** The keywords of JSON Schema 2020-12. */
31
+ export interface JsonSchemaKeywords {
32
+ readonly $schema?: string;
33
+ readonly $id?: string;
34
+ readonly $ref?: string;
35
+ readonly $anchor?: string;
36
+ readonly $dynamicRef?: string;
37
+ readonly $dynamicAnchor?: string;
38
+ readonly $vocabulary?: Readonly<Record<string, boolean>>;
39
+ readonly $comment?: string;
40
+ readonly $defs?: Readonly<Record<string, JsonSchema>>;
41
+ readonly type?: JsonSchemaType | readonly JsonSchemaType[];
42
+ readonly enum?: readonly unknown[];
43
+ readonly const?: unknown;
44
+ readonly multipleOf?: number;
45
+ readonly maximum?: number;
46
+ readonly exclusiveMaximum?: number;
47
+ readonly minimum?: number;
48
+ readonly exclusiveMinimum?: number;
49
+ readonly maxLength?: number;
50
+ readonly minLength?: number;
51
+ readonly pattern?: string;
52
+ readonly format?: string;
53
+ readonly contentEncoding?: string;
54
+ readonly contentMediaType?: string;
55
+ readonly contentSchema?: JsonSchema;
56
+ readonly prefixItems?: readonly JsonSchema[];
57
+ readonly items?: JsonSchema;
58
+ readonly contains?: JsonSchema;
59
+ readonly maxItems?: number;
60
+ readonly minItems?: number;
61
+ readonly uniqueItems?: boolean;
62
+ readonly maxContains?: number;
63
+ readonly minContains?: number;
64
+ readonly unevaluatedItems?: JsonSchema;
65
+ readonly properties?: Readonly<Record<string, JsonSchema>>;
66
+ readonly patternProperties?: Readonly<Record<string, JsonSchema>>;
67
+ readonly additionalProperties?: JsonSchema;
68
+ readonly unevaluatedProperties?: JsonSchema;
69
+ readonly propertyNames?: JsonSchema;
70
+ readonly required?: readonly string[];
71
+ readonly dependentRequired?: Readonly<Record<string, readonly string[]>>;
72
+ readonly dependentSchemas?: Readonly<Record<string, JsonSchema>>;
73
+ readonly maxProperties?: number;
74
+ readonly minProperties?: number;
75
+ readonly allOf?: readonly JsonSchema[];
76
+ readonly anyOf?: readonly JsonSchema[];
77
+ readonly oneOf?: readonly JsonSchema[];
78
+ readonly not?: JsonSchema;
79
+ readonly if?: JsonSchema;
80
+ readonly then?: JsonSchema;
81
+ readonly else?: JsonSchema;
82
+ readonly title?: string;
83
+ readonly description?: string;
84
+ readonly default?: unknown;
85
+ readonly deprecated?: boolean;
86
+ readonly readOnly?: boolean;
87
+ readonly writeOnly?: boolean;
88
+ readonly examples?: readonly unknown[];
89
+ readonly [extension: `x-${string}`]: unknown;
90
+ }
91
+ //# sourceMappingURL=json-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../src/json-schema.ts"],"names":[],"mappings":"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"}
@@ -12,8 +12,17 @@
12
12
  import type { AppOptions, RouteTableEntry } from "@tetsujs/core";
13
13
  import type { SchemaComponents } from "./components.ts";
14
14
  import type { OperationObject } from "./document.ts";
15
+ import type { Envelopes } from "./envelopes.ts";
15
16
  /** Describes one route. */
16
- export declare function operationOf(entry: RouteTableEntry, options: AppOptions, components: SchemaComponents, ids: OperationIds, warn: (message: string) => void): OperationObject;
17
+ export declare function operationOf(entry: RouteTableEntry, options: AppOptions, components: SchemaComponents, envelopes: Envelopes, ids: OperationIds, warn: (message: string) => void): OperationObject;
18
+ /**
19
+ * Records the envelopes a route declares, so that its definitions are the
20
+ * ones the document keeps — whichever operation refers to them first.
21
+ *
22
+ * Runs before any operation is built. A schema that cannot describe itself
23
+ * is skipped without a word: building the operation says so, once.
24
+ */
25
+ export declare function declareEnvelopes(entry: RouteTableEntry, envelopes: Envelopes): void;
17
26
  /**
18
27
  * Hands out the `operationId` of every route in one document.
19
28
  *
@@ -1 +1 @@
1
- {"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/operation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAEV,UAAU,EAEV,eAAe,EAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,eAAe,CAAC;AAKvB,2BAA2B;AAC3B,wBAAgB,WAAW,CACzB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,gBAAgB,EAC5B,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,CAkCjB;AAsED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,YAAY,IAAI,YAAY,CAmB3C"}
1
+ {"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/operation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAEV,UAAU,EAEV,eAAe,EAChB,MAAM,eAAe,CAAC;AAQvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAKhD,2BAA2B;AAC3B,wBAAgB,WAAW,CACzB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,CAyCjB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,SAAS,GACnB,IAAI,CAeN;AA+GD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,YAAY,IAAI,YAAY,CAmB3C"}
@@ -12,6 +12,13 @@
12
12
  * a parameter, so a self-hosted copy is one option away. Nothing is
13
13
  * fetched unless this function is called.
14
14
  *
15
+ * What the CDN serves runs on the application's origin, with its cookies.
16
+ * The default renderers are therefore pinned to an exact version and
17
+ * carry the hash of that file: a CDN, or a package release, that serves
18
+ * something else is refused by the browser rather than run. A page on an
19
+ * origin with a session is still somebody else's application in the
20
+ * user's session — which is why `docs()` can serve the document alone.
21
+ *
15
22
  * @module
16
23
  */
17
24
  /** The renderers this package knows how to bootstrap. */
@@ -20,6 +27,19 @@ export type DocsUi = "scalar" | "swagger-ui" | "redoc";
20
27
  export interface DocsAssets {
21
28
  readonly script: string;
22
29
  readonly style?: string;
30
+ /**
31
+ * Subresource Integrity hashes of the two files, for a copy that is not
32
+ * on the application's own origin: the browser runs the file only if it
33
+ * hashes to this. Absent, the files are loaded as they are.
34
+ *
35
+ * One line computes one:
36
+ * `curl -s <url> | openssl dgst -sha384 -binary | openssl base64 -A`,
37
+ * prefixed with `sha384-`.
38
+ */
39
+ readonly integrity?: {
40
+ readonly script?: string;
41
+ readonly style?: string;
42
+ };
23
43
  }
24
44
  /** What the page needs to know. */
25
45
  export interface DocsPageOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;AAEvD,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAgBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAyBzD"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;AAEvD,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAyBzD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tetsujs/openapi",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "An OpenAPI 3.1 document and docs page generated from Tetsu routes",
5
5
  "keywords": [
6
6
  "bun",
@@ -34,10 +34,10 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "@tetsujs/core": "^0.4.1"
37
+ "@tetsujs/core": "^0.5.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@tetsujs/core": "0.4.1"
40
+ "@tetsujs/core": "0.5.0"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { AnyHook, AnySchema } from "@tetsujs/core";
20
+ import type { JsonSchema } from "./json-schema.ts";
20
21
 
21
22
  /**
22
23
  * The definition of a scheme, as OpenAPI spells it.
@@ -83,6 +84,48 @@ export interface DocumentedResponse {
83
84
 
84
85
  /** An example of the envelope's `message`, for the same reason. */
85
86
  readonly message?: string;
87
+
88
+ /**
89
+ * Fields the hook adds to the envelope, as JSON Schema by name — the
90
+ * `retryAfter` of `{ ...errorBody(429, "RATE_LIMITED"), retryAfter }`.
91
+ *
92
+ * Each is documented as always present. The envelope's own `status`,
93
+ * `message` and `error` are not redefined by a field of the same name:
94
+ * `error` is what a client discriminates on, and it stays the code the
95
+ * hook declared. Ignored when the response has a `schema`, which says
96
+ * everything about the body itself.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * documented(limiter, {
101
+ * responses: [
102
+ * {
103
+ * status: 429,
104
+ * description: "Too many requests",
105
+ * error: "RATE_LIMITED",
106
+ * fields: { retryAfter: { type: "integer", minimum: 0 } },
107
+ * },
108
+ * ],
109
+ * });
110
+ * ```
111
+ */
112
+ readonly fields?: Readonly<Record<string, JsonSchema>>;
113
+
114
+ /**
115
+ * Headers the hook sets on this response, by name — the `retry-after`
116
+ * of a refusal.
117
+ *
118
+ * Documented as possible rather than required: a status several sources
119
+ * answer with is one response in the document, and a header one of them
120
+ * sets is not on the others'.
121
+ */
122
+ readonly headers?: Readonly<Record<string, DocumentedHeader>>;
123
+ }
124
+
125
+ /** A header a hook's response carries. */
126
+ export interface DocumentedHeader {
127
+ readonly description?: string;
128
+ readonly schema: JsonSchema;
86
129
  }
87
130
 
88
131
  /**
package/src/controller.ts CHANGED
@@ -16,27 +16,46 @@
16
16
  import type { App, RouteDef, SchemaConfig, ValidatePath } from "@tetsujs/core";
17
17
  import { onMount, route } from "@tetsujs/core";
18
18
  import type { OpenApiDocument } from "./document.ts";
19
+ import type { ErrorFormat } from "./errors.ts";
19
20
  import type { GeneratorWarning } from "./index.ts";
20
21
  import { openapi } from "./index.ts";
21
22
  import type { DocsAssets, DocsUi } from "./page.ts";
22
23
  import { docsPage } from "./page.ts";
23
24
 
24
25
  /** What the documentation controller serves, and from where. */
25
- export interface DocsOptions<Path extends string, UiPath extends string> {
26
+ export interface DocsOptions<
27
+ Path extends string,
28
+ UiPath extends string,
29
+ Ui extends DocsUi | false = DocsUi | false,
30
+ > {
26
31
  /** Title, version and the rest of the document's `info` block. */
27
32
  readonly info: OpenApiDocument["info"];
28
33
 
29
34
  /** Servers the API is reachable at. */
30
35
  readonly servers?: OpenApiDocument["servers"];
31
36
 
37
+ /**
38
+ * How the application's errors look, when not like the framework's
39
+ * envelope — see `errors` on {@link openapi}.
40
+ */
41
+ readonly errors?: ErrorFormat;
42
+
32
43
  /** Where the document is served. Defaults to `/openapi.json`. */
33
44
  readonly path?: Path & ValidatePath<Path>;
34
45
 
35
46
  /** Where the page is served. Defaults to `/docs`. */
36
47
  readonly uiPath?: UiPath & ValidatePath<UiPath>;
37
48
 
38
- /** Which renderer the page bootstraps. Defaults to `"scalar"`. */
39
- readonly ui?: DocsUi;
49
+ /**
50
+ * Which renderer the page bootstraps. Defaults to `"scalar"`.
51
+ *
52
+ * `false` serves the document without a page — what production wants
53
+ * on an origin that carries a session. The page runs its renderer on the
54
+ * application's origin, so a renderer that is not what it should be acts
55
+ * in the signed-in user's name there; the document alone runs nothing.
56
+ * `uiPath`, `title` and `assets` are then unused.
57
+ */
58
+ readonly ui?: Ui;
40
59
 
41
60
  /** Browser title of the page. Defaults to the document's own title. */
42
61
  readonly title?: string;
@@ -69,20 +88,27 @@ export interface DocsOptions<Path extends string, UiPath extends string> {
69
88
  * Instantiated through the factory, never directly — the routes carry the
70
89
  * configured paths in their types, which only the factory can infer.
71
90
  */
72
- export class DocsController<Path extends string, UiPath extends string> {
91
+ export class DocsController<
92
+ Path extends string,
93
+ UiPath extends string,
94
+ Page extends boolean = true,
95
+ > {
73
96
  /** The document itself, as `application/json`. */
74
97
  readonly json: RouteDef<Path, SchemaConfig, never, undefined, "GET">;
75
98
 
76
- /** The page that renders it. */
77
- readonly ui: RouteDef<UiPath, SchemaConfig, never, undefined, "GET">;
99
+ /** The page that renders it — none with `ui: false`. */
100
+ readonly ui: Page extends true
101
+ ? RouteDef<UiPath, SchemaConfig, never, undefined, "GET">
102
+ : undefined;
78
103
 
79
104
  private document: OpenApiDocument | undefined;
80
105
 
106
+ private page: string | undefined;
107
+
81
108
  constructor(
82
109
  private readonly options: DocsOptions<Path, UiPath>,
83
110
  documentPath: string,
84
111
  uiPath: string,
85
- page: string,
86
112
  ) {
87
113
  const hidden = !options.documentSelf;
88
114
 
@@ -98,7 +124,7 @@ export class DocsController<Path extends string, UiPath extends string> {
98
124
  path: "/docs",
99
125
  docs: { summary: "Rendered documentation", tags: ["docs"], hidden },
100
126
  handler: () =>
101
- new Response(page, {
127
+ new Response(this.page, {
102
128
  headers: { "content-type": "text/html; charset=utf-8" },
103
129
  }),
104
130
  });
@@ -113,18 +139,42 @@ export class DocsController<Path extends string, UiPath extends string> {
113
139
  * configured path lands at runtime.
114
140
  */
115
141
  this.json = { ...json, path: documentPath } as unknown as this["json"];
116
- this.ui = { ...ui, path: uiPath } as unknown as this["ui"];
142
+ this.ui = (
143
+ options.ui === false ? undefined : { ...ui, path: uiPath }
144
+ ) as this["ui"];
117
145
  }
118
146
 
119
- /** Generates the document of the application this was mounted in. */
147
+ /**
148
+ * Generates the document of the application this was mounted in, and
149
+ * the page that renders it.
150
+ *
151
+ * The page is built here rather than with the routes because only the
152
+ * application knows where the document ended up: a group this controller
153
+ * is mounted under prefixes its path, and a page pointing at the path as
154
+ * configured would fetch a `404`.
155
+ */
120
156
  [onMount](app: App): void {
121
157
  const generated = openapi(app, {
122
158
  info: this.options.info,
123
159
  ...(this.options.servers ? { servers: this.options.servers } : {}),
160
+ ...(this.options.errors ? { errors: this.options.errors } : {}),
124
161
  });
125
162
 
126
163
  this.document = generated.document;
127
164
 
165
+ const ui = this.options.ui ?? "scalar";
166
+ const json: unknown = this.json;
167
+ const served = app.entries.find((entry) => entry.def === json);
168
+
169
+ if (ui !== false && served) {
170
+ this.page = docsPage({
171
+ ui,
172
+ documentUrl: served.path,
173
+ title: this.options.title ?? this.options.info.title,
174
+ ...(this.options.assets ? { assets: this.options.assets } : {}),
175
+ });
176
+ }
177
+
128
178
  const report = this.options.onWarning ?? warn;
129
179
 
130
180
  for (const warning of generated.warnings) {
@@ -156,18 +206,15 @@ export class DocsController<Path extends string, UiPath extends string> {
156
206
  export function docs<
157
207
  const Path extends string = "/openapi.json",
158
208
  const UiPath extends string = "/docs",
159
- >(options: DocsOptions<Path, UiPath>): DocsController<Path, UiPath> {
160
- const documentPath = options.path ?? "/openapi.json";
161
- const uiPath = options.uiPath ?? "/docs";
162
-
163
- const page = docsPage({
164
- ui: options.ui ?? "scalar",
165
- documentUrl: documentPath,
166
- title: options.title ?? options.info.title,
167
- ...(options.assets ? { assets: options.assets } : {}),
168
- });
169
-
170
- return new DocsController(options, documentPath, uiPath, page);
209
+ const Ui extends DocsUi | false = "scalar",
210
+ >(
211
+ options: DocsOptions<Path, UiPath, Ui>,
212
+ ): DocsController<Path, UiPath, Ui extends false ? false : true> {
213
+ return new DocsController(
214
+ options,
215
+ options.path ?? "/openapi.json",
216
+ options.uiPath ?? "/docs",
217
+ ) as DocsController<Path, UiPath, Ui extends false ? false : true>;
171
218
  }
172
219
 
173
220
  function warn(warning: GeneratorWarning): void {
package/src/document.ts CHANGED
@@ -15,6 +15,8 @@
15
15
  * @module
16
16
  */
17
17
 
18
+ import type { JsonSchema } from "./json-schema.ts";
19
+
18
20
  /** Metadata every OpenAPI document must carry. */
19
21
  export interface DocumentInfo {
20
22
  readonly title: string;
@@ -44,9 +46,16 @@ export type ContentMap = Record<
44
46
  { readonly schema?: Record<string, unknown> }
45
47
  >;
46
48
 
49
+ /** A header a response carries. */
50
+ export interface HeaderObject {
51
+ readonly description?: string;
52
+ readonly schema: JsonSchema;
53
+ }
54
+
47
55
  /** One response of an operation. */
48
56
  export interface ResponseObject {
49
57
  readonly description: string;
58
+ readonly headers?: Record<string, HeaderObject>;
50
59
  readonly content?: ContentMap;
51
60
  }
52
61
 
package/src/emit.ts CHANGED
@@ -20,13 +20,15 @@ import { toJsonSchema } from "@tetsujs/core";
20
20
  const target = { target: "draft-2020-12" } as const;
21
21
 
22
22
  /**
23
- * A JSON Schema as the generator reads it: any keyword, with the two it
23
+ * A JSON Schema as the generator reads it: any keyword, with the ones it
24
24
  * looks into named, so reading them does not depend on the reader's
25
25
  * `noPropertyAccessFromIndexSignature`.
26
26
  */
27
27
  export interface JsonSchemaObject {
28
28
  properties?: unknown;
29
29
  required?: unknown;
30
+ anyOf?: unknown;
31
+ oneOf?: unknown;
30
32
  [keyword: string]: unknown;
31
33
  }
32
34