@tetsujs/openapi 0.3.0 → 0.4.1

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 CHANGED
@@ -16,7 +16,7 @@ import { docs } from "@tetsujs/openapi";
16
16
 
17
17
  createApp({
18
18
  routes: [
19
- group("/api", { children: [new UsersController(users)] }),
19
+ group("/api", { children: [usersController({ users })] }),
20
20
  docs({ info: { title: "Users API", version: "1.0.0" } }),
21
21
  ],
22
22
  });
@@ -53,11 +53,31 @@ routes are left out of the document.
53
53
  | `schema.body` and `bodyType` | the request body and its media types |
54
54
  | `schema.response` | responses — one, or one per status of a map; `null` is a status without a body |
55
55
  | `docs` | `summary`, `description`, `tags`, `deprecated` |
56
- | the controller and field | `operationId`: `setAvatar` on `UsersController` → `usersSetAvatar` |
56
+ | the controller's name and the field | `operationId`: `setAvatar` in `controller("Users", …)` → `usersSetAvatar` |
57
57
 
58
58
  `docs: { hidden: true }` leaves a route out of the document; it is served
59
59
  as before.
60
60
 
61
+ ## Operation ids
62
+
63
+ A generated client names its methods after the `operationId`s, so they are
64
+ a contract, and every one comes from a name you wrote:
65
+
66
+ | Route | `operationId` |
67
+ | --- | --- |
68
+ | with `docs: { operationId }` | as written |
69
+ | in `controller("Users", …)` as `setAvatar` | `usersSetAvatar` |
70
+ | in a class `UsersController` as `setAvatar` | `usersSetAvatar` |
71
+ | in an object literal as `setAvatar` | `setAvatar` |
72
+ | mounted on its own, `POST /auth/code` | `postAuthCode` |
73
+
74
+ Two routes arriving at the same id stop the application at startup, naming
75
+ both — nothing is renamed behind your back, which would change a method in
76
+ someone's SDK the day a route is added. Give one of them
77
+ `docs.operationId`, or its controller a name of its own. On a public API,
78
+ state the id on every route: it is then visible, and a change to it is a
79
+ change a reviewer sees.
80
+
61
81
  ```ts
62
82
  route({
63
83
  method: "POST",
@@ -115,6 +135,25 @@ Hooks from `@tetsujs/rate-limit` are already documented this way. A scheme
115
135
  is identified by its `name`: the same one mounted twice is one
116
136
  requirement, and two different schemes under one name are reported.
117
137
 
138
+ Every hook of a route runs, so every scheme its hooks carry is required
139
+ together: a route behind a CSRF check and a captcha is documented as
140
+ needing both, one entry of `security` — in OpenAPI, separate entries mean
141
+ any one of them will do. Two hooks of one scheme require the scopes of
142
+ both.
143
+
144
+ "Either" lives inside one hook: a hook that accepts a session cookie or a
145
+ bearer token says so with `anyOf`, and the document lists every
146
+ combination a client may bring:
147
+
148
+ ```ts
149
+ export const caller = secured(hook.beforeParse(sessionOrToken), {
150
+ anyOf: [cookieSession, bearerToken],
151
+ });
152
+
153
+ // with a CSRF check on the same route:
154
+ // security: [{ session: [], csrf: [] }, { bearer: [], csrf: [] }]
155
+ ```
156
+
118
157
  ## Using the document directly
119
158
 
120
159
  When the document itself is the output — written to a file in CI, fed to a
@@ -19,12 +19,12 @@ import type { AnyHook, AnySchema } from "@tetsujs/core";
19
19
  /**
20
20
  * The definition of a scheme, as OpenAPI spells it.
21
21
  *
22
- * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2` and
23
- * `openIdConnect` carry different fields, and narrowing them here would
24
- * be a second, poorer copy of the specification.
22
+ * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2`,
23
+ * `openIdConnect` and `mutualTLS` carry different fields, and narrowing
24
+ * them here would be a second, poorer copy of the specification.
25
25
  */
26
26
  export interface SecurityScheme {
27
- readonly type: "http" | "apiKey" | "oauth2" | "openIdConnect";
27
+ readonly type: "http" | "apiKey" | "oauth2" | "openIdConnect" | "mutualTLS";
28
28
  readonly [key: string]: unknown;
29
29
  }
30
30
  /** What a hook demands of a request, and what it answers when refused. */
@@ -71,10 +71,25 @@ export interface DocumentedResponse {
71
71
  /** An example of the envelope's `message`, for the same reason. */
72
72
  readonly message?: string;
73
73
  }
74
+ /**
75
+ * Requirements of which a hook accepts any one: a session cookie *or* a
76
+ * bearer token, checked by one hook.
77
+ *
78
+ * "Either" lives inside a hook, never between hooks — every hook of a
79
+ * route runs, so the schemes of two hooks are both required. A hook that
80
+ * takes either of two credentials says so here, and the document lists
81
+ * the combinations a client may bring.
82
+ */
83
+ export interface SecurityAlternatives {
84
+ readonly anyOf: readonly SecurityRequirement[];
85
+ }
74
86
  /** Everything a hook tells the generator about itself. */
75
87
  export interface HookDocs {
76
- /** What the hook demands of a request. */
77
- readonly security?: SecurityRequirement;
88
+ /**
89
+ * What the hook demands of a request: one requirement, or alternatives
90
+ * of which any one will do.
91
+ */
92
+ readonly security?: SecurityRequirement | SecurityAlternatives;
78
93
  /** What the hook can answer with. */
79
94
  readonly responses?: readonly DocumentedResponse[];
80
95
  }
@@ -100,6 +115,10 @@ export declare function documented<H extends AnyHook>(hook: H, docs: HookDocs):
100
115
  * Annotates a hook with the security it enforces — {@link documented} for
101
116
  * the case that comes up most.
102
117
  *
118
+ * One requirement, or `{ anyOf: [...] }` for a hook that accepts any one
119
+ * of several: the schemes of different hooks are all required, the
120
+ * alternatives of one hook are not.
121
+ *
103
122
  * @example
104
123
  * ```ts
105
124
  * export const auth = secured(
@@ -114,15 +133,37 @@ export declare function documented<H extends AnyHook>(hook: H, docs: HookDocs):
114
133
  * },
115
134
  * );
116
135
  * ```
136
+ *
137
+ * @example A hook that accepts a session cookie or a bearer token
138
+ * ```ts
139
+ * export const caller = secured(
140
+ * hook.beforeParse((ctx) => {
141
+ * const user = sessions.fromCookie(ctx) ?? tokens.fromHeader(ctx);
142
+ * if (!user) throw new HttpError(401);
143
+ * return { user };
144
+ * }),
145
+ * { anyOf: [cookieSession, bearerToken] },
146
+ * );
147
+ * ```
117
148
  */
118
- export declare function secured<H extends AnyHook>(hook: H, requirement: SecurityRequirement): H;
149
+ export declare function secured<H extends AnyHook>(hook: H, requirement: SecurityRequirement | SecurityAlternatives): H;
119
150
  /** Reads everything a hook was annotated with. */
120
151
  export declare function docsOf(hook: AnyHook): HookDocs | undefined;
121
- /** Reads the security requirement a hook carries, if it carries one. */
122
- export declare function securityOf(hook: AnyHook): SecurityRequirement | undefined;
152
+ /** Reads the security a hook was annotated with, if it was. */
153
+ export declare function securityOf(hook: AnyHook): SecurityRequirement | SecurityAlternatives | undefined;
123
154
  /** What the hooks of one route contribute, in execution order. */
124
155
  export interface HookContributions {
156
+ /**
157
+ * Every scheme the hooks mention, once per name, with the scopes of all
158
+ * of them: what the document registers, and what a refusal answers with.
159
+ */
125
160
  readonly security: readonly SecurityRequirement[];
161
+ /**
162
+ * What a request must satisfy, one condition per hook: every condition
163
+ * is required, and any one requirement of a condition satisfies it. A
164
+ * hook with one requirement is a condition of one.
165
+ */
166
+ readonly conditions: readonly (readonly SecurityRequirement[])[];
126
167
  readonly responses: readonly DocumentedResponse[];
127
168
  }
128
169
  /**
@@ -130,7 +171,9 @@ export interface HookContributions {
130
171
  *
131
172
  * A scheme is registered once per name and a response once per status and
132
173
  * description: the same guard on the application and on a group is one
133
- * requirement, not two.
174
+ * requirement, not two. Two hooks of one scheme asking for different
175
+ * scopes both run, so the requirement asks for the scopes of both — each
176
+ * once, in the order they were first asked for.
134
177
  *
135
178
  * Deduplicating by name means two hooks claiming one name with different
136
179
  * schemes lose one of them, and that is not a duplicate being collapsed —
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/annotations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,CAAC;IAC9D,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAEhC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEpC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,gDAAgD;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAE5B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,0DAA0D;AAC1D,MAAM,WAAW,QAAQ;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAExC,qCAAqC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACpD;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,GAAG,CAAC,CASxE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EACvC,IAAI,EAAE,CAAC,EACP,WAAW,EAAE,mBAAmB,GAC/B,CAAC,CAEH;AAED,kDAAkD;AAClD,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAE1D;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CAEzE;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,EAC1C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC/B,iBAAiB,CAyCnB"}
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/annotations.ts"],"names":[],"mappings":"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"}
@@ -74,7 +74,7 @@ export declare class DocsController<Path extends string, UiPath extends string>
74
74
  * ```ts
75
75
  * createApp({
76
76
  * routes: [
77
- * group("/api", { children: [new UsersController(users)] }),
77
+ * group("/api", { children: [usersController({ users })] }),
78
78
  * docs({ info: { title: "Users API", version: "1.0.0" } }),
79
79
  * ],
80
80
  * });
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { App } from "@tetsujs/core";
22
22
  import type { OpenApiDocument } from "./document.ts";
23
- export type { DocumentedResponse, HookContributions, HookDocs, SecurityRequirement, SecurityScheme, } from "./annotations.ts";
23
+ export type { DocumentedResponse, HookContributions, HookDocs, SecurityAlternatives, SecurityRequirement, SecurityScheme, } from "./annotations.ts";
24
24
  export { documented, secured } from "./annotations.ts";
25
25
  export type { DocsOptions } from "./controller.ts";
26
26
  export { DocsController, docs } from "./controller.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAC;AAIrE,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CAsF1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAC;AAIrE,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CAsF1E"}
package/dist/src/index.js CHANGED
@@ -17,6 +17,7 @@ function docsOf(hook) {
17
17
  }
18
18
  function contributionsOf(chains, warn) {
19
19
  const security = new Map;
20
+ const conditions = new Map;
20
21
  const responses = new Map;
21
22
  for (const slot of Object.values(chains)) {
22
23
  for (const hook of slot) {
@@ -25,11 +26,20 @@ function contributionsOf(chains, warn) {
25
26
  continue;
26
27
  }
27
28
  if (docs.security) {
28
- const claimed = security.get(docs.security.name);
29
- if (!claimed) {
30
- security.set(docs.security.name, docs.security);
31
- } else if (JSON.stringify(claimed.scheme) !== JSON.stringify(docs.security.scheme)) {
32
- warn?.(`two hooks claim the security scheme "${docs.security.name}" with different definitions; one of them is not described`);
29
+ const alternatives = alternativesOf(docs.security);
30
+ conditions.set(conditionKey(alternatives), alternatives);
31
+ for (const requirement of alternatives) {
32
+ const claimed = security.get(requirement.name);
33
+ if (!claimed) {
34
+ security.set(requirement.name, requirement);
35
+ } else if (JSON.stringify(claimed.scheme) === JSON.stringify(requirement.scheme)) {
36
+ security.set(requirement.name, {
37
+ ...claimed,
38
+ scopes: joinScopes(claimed.scopes, requirement.scopes)
39
+ });
40
+ } else {
41
+ warn?.(`two hooks claim the security scheme "${requirement.name}" with different definitions; one of them is not described`);
42
+ }
33
43
  }
34
44
  }
35
45
  for (const response of docs.responses ?? []) {
@@ -42,9 +52,28 @@ function contributionsOf(chains, warn) {
42
52
  }
43
53
  return {
44
54
  security: [...security.values()],
55
+ conditions: [...conditions.values()],
45
56
  responses: [...responses.values()]
46
57
  };
47
58
  }
59
+ function alternativesOf(security) {
60
+ return "anyOf" in security ? security.anyOf : [security];
61
+ }
62
+ function conditionKey(alternatives) {
63
+ return JSON.stringify(alternatives.map((requirement) => [
64
+ requirement.name,
65
+ [...requirement.scopes ?? []].sort()
66
+ ]));
67
+ }
68
+ function joinScopes(first, second) {
69
+ const joined = [...first ?? []];
70
+ for (const scope of second ?? []) {
71
+ if (!joined.includes(scope)) {
72
+ joined.push(scope);
73
+ }
74
+ }
75
+ return joined;
76
+ }
48
77
 
49
78
  // packages/openapi/src/components.ts
50
79
  function schemaComponents() {
@@ -181,44 +210,68 @@ function operationOf(entry, options, components, ids, warn) {
181
210
  ...entry.def.docs?.deprecated === undefined ? {} : { deprecated: entry.def.docs.deprecated },
182
211
  ...parameters.length > 0 ? { parameters } : {},
183
212
  ...body ? { requestBody: body } : {},
184
- ...contributed.security.length > 0 ? {
185
- security: contributed.security.map((requirement) => ({
186
- [requirement.name]: requirement.scopes ?? []
187
- }))
188
- } : {},
213
+ ...contributed.security.length > 0 ? { security: securityOf(contributed.conditions) } : {},
189
214
  responses: responses(entry, options, contributed, components, warn)
190
215
  };
191
216
  }
217
+ function securityOf(conditions) {
218
+ let combinations = [{}];
219
+ for (const alternatives of conditions) {
220
+ combinations = combinations.flatMap((combination) => alternatives.map((requirement) => ({
221
+ ...combination,
222
+ [requirement.name]: joined(combination[requirement.name], requirement.scopes)
223
+ })));
224
+ }
225
+ const seen = new Set;
226
+ return combinations.filter((combination) => {
227
+ const key = JSON.stringify(Object.entries(combination).map(([name, scopes]) => [name, [...scopes].sort()]).sort());
228
+ if (seen.has(key)) {
229
+ return false;
230
+ }
231
+ seen.add(key);
232
+ return true;
233
+ });
234
+ }
235
+ function joined(present, added) {
236
+ const scopes = [...present ?? []];
237
+ for (const scope of added ?? []) {
238
+ if (!scopes.includes(scope)) {
239
+ scopes.push(scope);
240
+ }
241
+ }
242
+ return scopes;
243
+ }
192
244
  function operationIds() {
193
- const taken = new Set;
245
+ const taken = new Map;
194
246
  return {
195
247
  take(entry) {
196
- const preferred = declaredId(entry) ?? derivedId(entry);
197
- if (!taken.has(preferred)) {
198
- taken.add(preferred);
199
- return preferred;
200
- }
201
- const derived = derivedId(entry);
202
- if (!taken.has(derived)) {
203
- taken.add(derived);
204
- return derived;
248
+ const id = idOf(entry);
249
+ const holder = taken.get(id);
250
+ if (holder !== undefined) {
251
+ throw new Error(`operationId "${id}" is taken by both ${describe(holder)} and ${describe(entry)} \u2014 a generated client would get one method for two operations. Give one of them docs.operationId, or its controller a name of its own.`);
205
252
  }
206
- let name = derived;
207
- for (let attempt = 2;taken.has(name); attempt += 1) {
208
- name = `${derived}${attempt}`;
209
- }
210
- taken.add(name);
211
- return name;
253
+ taken.set(id, entry);
254
+ return id;
212
255
  }
213
256
  };
214
257
  }
215
- function declaredId(entry) {
258
+ function idOf(entry) {
259
+ const stated = entry.def.docs?.operationId;
260
+ if (stated !== undefined) {
261
+ return stated;
262
+ }
216
263
  if (entry.name === undefined) {
217
- return;
264
+ return derivedId(entry);
265
+ }
266
+ if (entry.controller === undefined) {
267
+ return entry.name;
218
268
  }
219
269
  const controller = entry.controller.replace(/Controller$/, "");
220
270
  return `${lowerFirst(controller)}${capitalize(entry.name)}`;
221
271
  }
272
+ function describe(entry) {
273
+ return `${entry.method} ${entry.path}`;
274
+ }
222
275
  function derivedId(entry) {
223
276
  return `${entry.method.toLowerCase()}${entry.path.split(/[/:*]/).filter(Boolean).map(capitalize).join("")}`;
224
277
  }
@@ -612,5 +665,5 @@ export {
612
665
  secured
613
666
  };
614
667
 
615
- //# debugId=698CDE5A6513C23964756E2164756E21
668
+ //# debugId=AFF7EFC95370A0C664756E2164756E21
616
669
  //# sourceMappingURL=index.js.map
@@ -2,16 +2,16 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/annotations.ts", "../src/components.ts", "../src/emit.ts", "../src/parameters.ts", "../src/operation.ts", "../src/controller.ts", "../src/page.ts", "../src/index.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * What hooks contribute to the documentation.\n *\n * What a route is protected by, or answers with when a guard refuses, is\n * not in its config: a hook is a function in a slot, and the route that\n * mounts it says nothing about tokens or rate limits. The contribution is\n * therefore annotated onto the hook — from this package, not from\n * `hook.*` — so the core keeps knowing nothing about OpenAPI and the\n * factories stay free of documentation options.\n *\n * A hook annotated once is documented everywhere it runs: application and\n * group chains are merged into every route's chains at startup, so a guard\n * mounted on a zone documents the zone's operations without repeating\n * itself.\n *\n * @module\n */\n\nimport type { AnyHook, AnySchema } from \"@tetsujs/core\";\n\n/**\n * The definition of a scheme, as OpenAPI spells it.\n *\n * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2` and\n * `openIdConnect` carry different fields, and narrowing them here would\n * be a second, poorer copy of the specification.\n */\nexport interface SecurityScheme {\n readonly type: \"http\" | \"apiKey\" | \"oauth2\" | \"openIdConnect\";\n readonly [key: string]: unknown;\n}\n\n/** What a hook demands of a request, and what it answers when refused. */\nexport interface SecurityRequirement {\n /** The name the scheme is registered under in `components`. */\n readonly name: string;\n\n /** The scheme itself, registered once per name. */\n readonly scheme: SecurityScheme;\n\n /** OAuth2 scopes; empty for every other type. */\n readonly scopes?: readonly string[];\n\n /** The status an unauthenticated request gets. Defaults to `401`. */\n readonly status?: number;\n\n /** How the refusal is described in the document. */\n readonly description?: string;\n\n /**\n * The `error` code the refusal carries, when the guard answers with the\n * framework's envelope — `\"UNAUTHORIZED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const` for the same reason as on a\n * {@link DocumentedResponse}: only the guard knows its own code.\n */\n readonly error?: string;\n\n /** An example of the refusal's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/**\n * One response a hook can produce, which the route's own schema knows\n * nothing about — a rate limiter's `429`, a guard's `401`.\n */\nexport interface DocumentedResponse {\n readonly status: number;\n readonly description: string;\n\n /** The body, when the hook answers with one. */\n readonly schema?: AnySchema;\n\n /**\n * The `error` code of the failure envelope, when the hook answers with\n * the framework's shape — `\"RATE_LIMITED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const`, so a client can discriminate on it. Only the\n * hook knows its own code; without one the document says the field is a\n * string and no more.\n */\n readonly error?: string;\n\n /** An example of the envelope's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/** Everything a hook tells the generator about itself. */\nexport interface HookDocs {\n /** What the hook demands of a request. */\n readonly security?: SecurityRequirement;\n\n /** What the hook can answer with. */\n readonly responses?: readonly DocumentedResponse[];\n}\n\nconst docsKey = \"~tetsu/hook-docs\";\n\n/**\n * Annotates a hook with what it contributes to the documentation.\n *\n * Returns a copy: what the caller exports is what carries the annotation,\n * and a hook used elsewhere is not changed behind its author's back. The\n * type is preserved exactly, so an annotated hook goes into a stack like\n * any other.\n *\n * @example\n * ```ts\n * export const limiter = documented(hook.beforeParse(check), {\n * responses: [\n * { status: 429, description: \"Rate limit exceeded\", schema: TooMany },\n * ],\n * });\n * ```\n */\nexport function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {\n const annotated = { ...hook };\n\n Object.defineProperty(annotated, docsKey, {\n enumerable: false,\n value: { ...docsOf(hook), ...docs },\n });\n\n return annotated as H;\n}\n\n/**\n * Annotates a hook with the security it enforces — {@link documented} for\n * the case that comes up most.\n *\n * @example\n * ```ts\n * export const auth = secured(\n * hook.beforeParse((ctx) => {\n * const user = verify(ctx.req.headers.get(\"authorization\"));\n * if (!user) throw new HttpError(401);\n * return { user };\n * }),\n * {\n * name: \"bearerAuth\",\n * scheme: { type: \"http\", scheme: \"bearer\", bearerFormat: \"JWT\" },\n * },\n * );\n * ```\n */\nexport function secured<H extends AnyHook>(\n hook: H,\n requirement: SecurityRequirement,\n): H {\n return documented(hook, { security: requirement });\n}\n\n/** Reads everything a hook was annotated with. */\nexport function docsOf(hook: AnyHook): HookDocs | undefined {\n return (hook as unknown as Record<string, HookDocs | undefined>)[docsKey];\n}\n\n/** Reads the security requirement a hook carries, if it carries one. */\nexport function securityOf(hook: AnyHook): SecurityRequirement | undefined {\n return docsOf(hook)?.security;\n}\n\n/** What the hooks of one route contribute, in execution order. */\nexport interface HookContributions {\n readonly security: readonly SecurityRequirement[];\n readonly responses: readonly DocumentedResponse[];\n}\n\n/**\n * Collects the contributions of a route's chains.\n *\n * A scheme is registered once per name and a response once per status and\n * description: the same guard on the application and on a group is one\n * requirement, not two.\n *\n * Deduplicating by name means two hooks claiming one name with different\n * schemes lose one of them, and that is not a duplicate being collapsed —\n * it is a requirement the route really has going undescribed. Passing\n * `warn` is how the caller hears about it; the generator does.\n */\nexport function contributionsOf(\n chains: Record<string, readonly AnyHook[]>,\n warn?: (message: string) => void,\n): HookContributions {\n const security = new Map<string, SecurityRequirement>();\n const responses = new Map<string, DocumentedResponse>();\n\n for (const slot of Object.values(chains)) {\n for (const hook of slot) {\n const docs = docsOf(hook);\n\n if (!docs) {\n continue;\n }\n\n if (docs.security) {\n const claimed = security.get(docs.security.name);\n\n if (!claimed) {\n security.set(docs.security.name, docs.security);\n } else if (\n JSON.stringify(claimed.scheme) !==\n JSON.stringify(docs.security.scheme)\n ) {\n warn?.(\n `two hooks claim the security scheme \"${docs.security.name}\" with different definitions; one of them is not described`,\n );\n }\n }\n\n for (const response of docs.responses ?? []) {\n const key = `${response.status} ${response.description}`;\n\n if (!responses.has(key)) {\n responses.set(key, response);\n }\n }\n }\n }\n\n return {\n security: [...security.values()],\n responses: [...responses.values()],\n };\n}\n",
5
+ "/**\n * What hooks contribute to the documentation.\n *\n * What a route is protected by, or answers with when a guard refuses, is\n * not in its config: a hook is a function in a slot, and the route that\n * mounts it says nothing about tokens or rate limits. The contribution is\n * therefore annotated onto the hook — from this package, not from\n * `hook.*` — so the core keeps knowing nothing about OpenAPI and the\n * factories stay free of documentation options.\n *\n * A hook annotated once is documented everywhere it runs: application and\n * group chains are merged into every route's chains at startup, so a guard\n * mounted on a zone documents the zone's operations without repeating\n * itself.\n *\n * @module\n */\n\nimport type { AnyHook, AnySchema } from \"@tetsujs/core\";\n\n/**\n * The definition of a scheme, as OpenAPI spells it.\n *\n * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2`,\n * `openIdConnect` and `mutualTLS` carry different fields, and narrowing\n * them here would be a second, poorer copy of the specification.\n */\nexport interface SecurityScheme {\n readonly type: \"http\" | \"apiKey\" | \"oauth2\" | \"openIdConnect\" | \"mutualTLS\";\n readonly [key: string]: unknown;\n}\n\n/** What a hook demands of a request, and what it answers when refused. */\nexport interface SecurityRequirement {\n /** The name the scheme is registered under in `components`. */\n readonly name: string;\n\n /** The scheme itself, registered once per name. */\n readonly scheme: SecurityScheme;\n\n /** OAuth2 scopes; empty for every other type. */\n readonly scopes?: readonly string[];\n\n /** The status an unauthenticated request gets. Defaults to `401`. */\n readonly status?: number;\n\n /** How the refusal is described in the document. */\n readonly description?: string;\n\n /**\n * The `error` code the refusal carries, when the guard answers with the\n * framework's envelope — `\"UNAUTHORIZED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const` for the same reason as on a\n * {@link DocumentedResponse}: only the guard knows its own code.\n */\n readonly error?: string;\n\n /** An example of the refusal's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/**\n * One response a hook can produce, which the route's own schema knows\n * nothing about — a rate limiter's `429`, a guard's `401`.\n */\nexport interface DocumentedResponse {\n readonly status: number;\n readonly description: string;\n\n /** The body, when the hook answers with one. */\n readonly schema?: AnySchema;\n\n /**\n * The `error` code of the failure envelope, when the hook answers with\n * the framework's shape — `\"RATE_LIMITED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const`, so a client can discriminate on it. Only the\n * hook knows its own code; without one the document says the field is a\n * string and no more.\n */\n readonly error?: string;\n\n /** An example of the envelope's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/**\n * Requirements of which a hook accepts any one: a session cookie *or* a\n * bearer token, checked by one hook.\n *\n * \"Either\" lives inside a hook, never between hooks — every hook of a\n * route runs, so the schemes of two hooks are both required. A hook that\n * takes either of two credentials says so here, and the document lists\n * the combinations a client may bring.\n */\nexport interface SecurityAlternatives {\n readonly anyOf: readonly SecurityRequirement[];\n}\n\n/** Everything a hook tells the generator about itself. */\nexport interface HookDocs {\n /**\n * What the hook demands of a request: one requirement, or alternatives\n * of which any one will do.\n */\n readonly security?: SecurityRequirement | SecurityAlternatives;\n\n /** What the hook can answer with. */\n readonly responses?: readonly DocumentedResponse[];\n}\n\nconst docsKey = \"~tetsu/hook-docs\";\n\n/**\n * Annotates a hook with what it contributes to the documentation.\n *\n * Returns a copy: what the caller exports is what carries the annotation,\n * and a hook used elsewhere is not changed behind its author's back. The\n * type is preserved exactly, so an annotated hook goes into a stack like\n * any other.\n *\n * @example\n * ```ts\n * export const limiter = documented(hook.beforeParse(check), {\n * responses: [\n * { status: 429, description: \"Rate limit exceeded\", schema: TooMany },\n * ],\n * });\n * ```\n */\nexport function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {\n const annotated = { ...hook };\n\n Object.defineProperty(annotated, docsKey, {\n enumerable: false,\n value: { ...docsOf(hook), ...docs },\n });\n\n return annotated as H;\n}\n\n/**\n * Annotates a hook with the security it enforces — {@link documented} for\n * the case that comes up most.\n *\n * One requirement, or `{ anyOf: [...] }` for a hook that accepts any one\n * of several: the schemes of different hooks are all required, the\n * alternatives of one hook are not.\n *\n * @example\n * ```ts\n * export const auth = secured(\n * hook.beforeParse((ctx) => {\n * const user = verify(ctx.req.headers.get(\"authorization\"));\n * if (!user) throw new HttpError(401);\n * return { user };\n * }),\n * {\n * name: \"bearerAuth\",\n * scheme: { type: \"http\", scheme: \"bearer\", bearerFormat: \"JWT\" },\n * },\n * );\n * ```\n *\n * @example A hook that accepts a session cookie or a bearer token\n * ```ts\n * export const caller = secured(\n * hook.beforeParse((ctx) => {\n * const user = sessions.fromCookie(ctx) ?? tokens.fromHeader(ctx);\n * if (!user) throw new HttpError(401);\n * return { user };\n * }),\n * { anyOf: [cookieSession, bearerToken] },\n * );\n * ```\n */\nexport function secured<H extends AnyHook>(\n hook: H,\n requirement: SecurityRequirement | SecurityAlternatives,\n): H {\n return documented(hook, { security: requirement });\n}\n\n/** Reads everything a hook was annotated with. */\nexport function docsOf(hook: AnyHook): HookDocs | undefined {\n return (hook as unknown as Record<string, HookDocs | undefined>)[docsKey];\n}\n\n/** Reads the security a hook was annotated with, if it was. */\nexport function securityOf(\n hook: AnyHook,\n): SecurityRequirement | SecurityAlternatives | undefined {\n return docsOf(hook)?.security;\n}\n\n/** What the hooks of one route contribute, in execution order. */\nexport interface HookContributions {\n /**\n * Every scheme the hooks mention, once per name, with the scopes of all\n * of them: what the document registers, and what a refusal answers with.\n */\n readonly security: readonly SecurityRequirement[];\n\n /**\n * What a request must satisfy, one condition per hook: every condition\n * is required, and any one requirement of a condition satisfies it. A\n * hook with one requirement is a condition of one.\n */\n readonly conditions: readonly (readonly SecurityRequirement[])[];\n\n readonly responses: readonly DocumentedResponse[];\n}\n\n/**\n * Collects the contributions of a route's chains.\n *\n * A scheme is registered once per name and a response once per status and\n * description: the same guard on the application and on a group is one\n * requirement, not two. Two hooks of one scheme asking for different\n * scopes both run, so the requirement asks for the scopes of both — each\n * once, in the order they were first asked for.\n *\n * Deduplicating by name means two hooks claiming one name with different\n * schemes lose one of them, and that is not a duplicate being collapsed —\n * it is a requirement the route really has going undescribed. Passing\n * `warn` is how the caller hears about it; the generator does.\n */\nexport function contributionsOf(\n chains: Record<string, readonly AnyHook[]>,\n warn?: (message: string) => void,\n): HookContributions {\n const security = new Map<string, SecurityRequirement>();\n const conditions = new Map<string, readonly SecurityRequirement[]>();\n const responses = new Map<string, DocumentedResponse>();\n\n for (const slot of Object.values(chains)) {\n for (const hook of slot) {\n const docs = docsOf(hook);\n\n if (!docs) {\n continue;\n }\n\n if (docs.security) {\n const alternatives = alternativesOf(docs.security);\n\n conditions.set(conditionKey(alternatives), alternatives);\n\n for (const requirement of alternatives) {\n const claimed = security.get(requirement.name);\n\n if (!claimed) {\n security.set(requirement.name, requirement);\n } else if (\n JSON.stringify(claimed.scheme) ===\n JSON.stringify(requirement.scheme)\n ) {\n security.set(requirement.name, {\n ...claimed,\n scopes: joinScopes(claimed.scopes, requirement.scopes),\n });\n } else {\n warn?.(\n `two hooks claim the security scheme \"${requirement.name}\" with different definitions; one of them is not described`,\n );\n }\n }\n }\n\n for (const response of docs.responses ?? []) {\n const key = `${response.status} ${response.description}`;\n\n if (!responses.has(key)) {\n responses.set(key, response);\n }\n }\n }\n }\n\n return {\n security: [...security.values()],\n conditions: [...conditions.values()],\n responses: [...responses.values()],\n };\n}\n\n/** A hook's security as the alternatives it accepts; one is one. */\nfunction alternativesOf(\n security: SecurityRequirement | SecurityAlternatives,\n): readonly SecurityRequirement[] {\n return \"anyOf\" in security ? security.anyOf : [security];\n}\n\n/**\n * What makes two conditions the same one: the same guard mounted on the\n * application and on a group is one condition, and counting it twice would\n * multiply its alternatives with themselves.\n */\nfunction conditionKey(alternatives: readonly SecurityRequirement[]): string {\n return JSON.stringify(\n alternatives.map((requirement) => [\n requirement.name,\n [...(requirement.scopes ?? [])].sort(),\n ]),\n );\n}\n\n/** The scopes of two requirements of one scheme, each once, in order. */\nfunction joinScopes(\n first: readonly string[] | undefined,\n second: readonly string[] | undefined,\n): readonly string[] {\n const joined = [...(first ?? [])];\n\n for (const scope of second ?? []) {\n if (!joined.includes(scope)) {\n joined.push(scope);\n }\n }\n\n return joined;\n}\n",
6
6
  "/**\n * The document's `components/schemas` section.\n *\n * The failures the framework fills in repeat across every operation — a\n * `500` on all of them, a `413` on every route with a body — and inlining\n * the same envelope each time makes a document that is mostly boilerplate.\n * They are defined once here and referenced with `$ref`, which is also how\n * a client generator ends up with one `MalformedJson` type instead of one\n * per endpoint.\n *\n * Only the framework's own shapes go here. A schema that came from a\n * validator is still inlined: it is the author's, it has no name the\n * generator could give it, and `$ref` would invent one.\n *\n * @module\n */\n\n/** A reference to a definition in `components/schemas`. */\nexport interface SchemaRef {\n readonly $ref: string;\n}\n\n/** The definitions collected while generating a document. */\nexport interface SchemaComponents {\n /**\n * Registers a schema and returns the reference to it.\n *\n * Identical schemas share one definition, whatever name each caller\n * asked for; different schemas that want the same name get a numbered\n * one, so a document is never silently wrong about what a `$ref` points\n * at.\n */\n ref(preferred: string, schema: Record<string, unknown>): SchemaRef;\n\n /** What was registered, in registration order. */\n readonly schemas: Record<string, unknown>;\n}\n\nexport function schemaComponents(): SchemaComponents {\n const schemas: Record<string, unknown> = {};\n const registered = new Map<string, string>();\n\n return {\n schemas,\n\n ref(preferred: string, schema: Record<string, unknown>): SchemaRef {\n const fingerprint = JSON.stringify(schema);\n const already = registered.get(fingerprint);\n\n if (already) {\n return { $ref: `#/components/schemas/${already}` };\n }\n\n let name = preferred;\n\n for (let attempt = 2; name in schemas; attempt += 1) {\n name = `${preferred}${attempt}`;\n }\n\n schemas[name] = schema;\n registered.set(fingerprint, name);\n\n return { $ref: `#/components/schemas/${name}` };\n },\n };\n}\n\n/**\n * The name a failure is defined under: its `error` code in PascalCase —\n * `MALFORMED_JSON` becomes `MalformedJson` — so the definition and the code\n * a client branches on read as the same thing.\n *\n * A failure whose code only the hook knows, and which the hook did not\n * declare, is named after its status instead.\n */\nexport function failureName(status: number, error?: string): string {\n const name = (error ?? \"\")\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n\n // A code with nothing ASCII in it sanitizes to nothing — a code in\n // Cyrillic or Japanese, or one that is all punctuation. That is the same\n // situation as no code at all, and gets the same name: the alternative\n // was a component keyed `\"\"`, which the specification does not allow, and\n // a second such code keyed `\"2\"` by the collision numbering below it.\n return name || `Failure${status}`;\n}\n",
7
7
  "/**\n * Asking a schema to describe itself, without letting it stop the document.\n *\n * Internal to the package.\n *\n * @module\n */\n\nimport { toJsonSchema } from \"@tetsujs/core\";\n\n/**\n * The JSON Schema dialect a schema is asked for.\n *\n * OpenAPI 3.1 takes JSON Schema 2020-12 as it is, and `draft-2020-12` is a\n * target the Standard JSON Schema specification names — so every library\n * that implements it answers. This used to be `openapi-3.1`, which only\n * Zod accepts: ArkType and Valibot refused it, and their bodies went into\n * the document without a shape and with a warning each.\n */\nconst target = { target: \"draft-2020-12\" } as const;\n\n/**\n * A JSON Schema as the generator reads it: any keyword, with the two it\n * looks into named, so reading them does not depend on the reader's\n * `noPropertyAccessFromIndexSignature`.\n */\nexport interface JsonSchemaObject {\n properties?: unknown;\n required?: unknown;\n [keyword: string]: unknown;\n}\n\n/** Which shape of a schema to describe: what arrives, or what leaves. */\ntype Direction = \"input\" | \"output\";\n\n/**\n * The JSON Schema of a declaration, or `undefined` and a warning saying why.\n *\n * A schema can fail to describe itself in two ways, and the generator owes\n * the same answer to both: an adapter without JSON Schema support returns\n * nothing, and an adapter that has it can still throw — a converter meeting\n * a shape it cannot express, a structure too deep to walk, a `$ref` that\n * leads back to where it started.\n *\n * Only the first was handled, and the difference between them is not the\n * document: it is whether the application starts at all. `docs()` builds\n * the document from `onMount`, which `createApp` calls while it is still\n * assembling itself, so an escaping error is not a gap in a description —\n * it is a server that never listens, over a route that would have worked.\n *\n * `subject` names the declaration for the message, as the reader of a\n * warning would name it: `\"the body\"`, `\"a response\"`.\n */\nexport function emitted(\n schema: unknown,\n subject: string,\n warn: (message: string) => void,\n direction: Direction = \"output\",\n): JsonSchemaObject | undefined {\n let described: JsonSchemaObject | undefined;\n\n try {\n described = toJsonSchema(schema, target, direction);\n } catch (error) {\n warn(`${subject} schema failed to emit JSON Schema: ${reason(error)}`);\n\n return undefined;\n }\n\n if (!described) {\n warn(`${subject} schema does not emit JSON Schema`);\n\n return undefined;\n }\n\n return withoutDialect(described);\n}\n\n/**\n * A schema as the document embeds it: without its own `$schema`.\n *\n * Asked for draft 2020-12, Zod and ArkType name the dialect at the root of\n * every schema they emit. The document declares it once for all of them,\n * and the same line on every body and response is noise.\n */\nfunction withoutDialect(schema: JsonSchemaObject): JsonSchemaObject {\n if (!(\"$schema\" in schema)) {\n return schema;\n }\n\n const { $schema: _, ...rest } = schema;\n\n return rest;\n}\n\nfunction reason(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n",
8
8
  "/**\n * Path templates and request parameters.\n *\n * The framework's path syntax is Bun's — `:param` and a trailing `*` —\n * while OpenAPI templates variables as `{param}`. The conversion happens\n * here, together with the parameter objects the template then requires:\n * OpenAPI demands that every variable in a path be declared.\n *\n * @module\n */\n\nimport type { AnySchema } from \"@tetsujs/core\";\nimport type { ParameterObject } from \"./document.ts\";\nimport { emitted } from \"./emit.ts\";\n\n/** The name a wildcard segment is documented under. */\nexport const wildcardName = \"wildcard\";\n\n/**\n * Rewrites a route path as an OpenAPI path template.\n *\n * A wildcard becomes an ordinary variable: OpenAPI has no syntax for\n * \"everything below here\", so the closest honest template is a named\n * segment, documented as such.\n */\nexport function toTemplate(path: string): string {\n return path\n .split(\"/\")\n .map((segment) => {\n if (segment.startsWith(\":\")) {\n return `{${segment.slice(1)}}`;\n }\n\n return segment === \"*\" ? `{${wildcardName}}` : segment;\n })\n .join(\"/\");\n}\n\n/** The variables a path template declares, in order. */\nexport function templateNames(path: string): string[] {\n const names: string[] = [];\n\n for (const segment of path.split(\"/\")) {\n if (segment.startsWith(\":\")) {\n names.push(segment.slice(1));\n } else if (segment === \"*\") {\n names.push(wildcardName);\n }\n }\n\n return names;\n}\n\n/**\n * Describes the path parameters of a route.\n *\n * A `params` schema describes the whole object, so each variable takes the\n * property of the same name; without one — or without a property for a\n * given variable — the value is what the router captured: a string.\n */\nexport function pathParameters(\n path: string,\n schema: AnySchema | undefined,\n warn: (message: string) => void,\n): ParameterObject[] {\n const properties = objectProperties(schema, \"params\", warn);\n\n return templateNames(path).map((name) => ({\n name,\n in: \"path\" as const,\n required: true,\n ...(name === wildcardName\n ? {\n description:\n \"Matches the rest of the path, including further segments.\",\n }\n : {}),\n schema: properties[name] ?? { type: \"string\" },\n }));\n}\n\n/**\n * Describes the query, header or cookie parameters of a route.\n *\n * All three are objects at the schema level and lists of parameters in a\n * document, so each declared property becomes one parameter, required when\n * the schema says so.\n *\n * A session cookie is the exception the specification makes itself: a\n * credential belongs in `securitySchemes` as `apiKey in: cookie`, not\n * among the parameters. Nothing here can tell one cookie from the other —\n * that is what an annotation on the hook reading it is for, the same way\n * `secured()` marks an authenticating hook.\n */\nexport function partParameters(\n schema: AnySchema | undefined,\n where: \"query\" | \"header\" | \"cookie\",\n warn: (message: string) => void,\n): ParameterObject[] {\n if (!schema) {\n return [];\n }\n\n const described = emitted(schema, `the ${where}`, warn, \"input\");\n\n if (!described) {\n return [];\n }\n\n const properties = (described.properties ?? {}) as Record<\n string,\n Record<string, unknown>\n >;\n const required = new Set(\n Array.isArray(described.required) ? (described.required as string[]) : [],\n );\n\n return Object.entries(properties).map(([name, property]) => ({\n name,\n in: where,\n required: required.has(name),\n schema: property,\n }));\n}\n\nfunction objectProperties(\n schema: AnySchema | undefined,\n where: string,\n warn: (message: string) => void,\n): Record<string, Record<string, unknown>> {\n if (!schema) {\n return {};\n }\n\n const described = emitted(schema, `the ${where}`, warn, \"input\");\n\n if (!described) {\n return {};\n }\n\n return (described.properties ?? {}) as Record<\n string,\n Record<string, unknown>\n >;\n}\n",
9
- "/**\n * One route table entry, described as an OpenAPI operation.\n *\n * Bodies and responses live here; parameters are in `parameters.ts`. The\n * error responses a route can produce without the author writing anything\n * — a rejected schema, an unparsable body, a body over the limit — are\n * derived from what the route declares and from the options the\n * application actually runs with.\n *\n * @module\n */\n\nimport type {\n AnySchema,\n AppOptions,\n BodyType,\n RouteTableEntry,\n} from \"@tetsujs/core\";\nimport type { HookContributions } from \"./annotations.ts\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport type { SchemaComponents } from \"./components.ts\";\nimport { failureName } from \"./components.ts\";\nimport type {\n ContentMap,\n OperationObject,\n ResponseObject,\n} from \"./document.ts\";\nimport type { JsonSchemaObject } from \"./emit.ts\";\nimport { emitted } from \"./emit.ts\";\nimport { partParameters, pathParameters } from \"./parameters.ts\";\n\n/** Describes one route. */\nexport function operationOf(\n entry: RouteTableEntry,\n options: AppOptions,\n components: SchemaComponents,\n ids: OperationIds,\n warn: (message: string) => void,\n): OperationObject {\n const schema = entry.def.schema;\n\n const parameters = [\n ...pathParameters(entry.path, schema?.params, warn),\n ...partParameters(schema?.query, \"query\", warn),\n ...partParameters(schema?.headers, \"header\", warn),\n ...partParameters(schema?.cookies, \"cookie\", warn),\n ];\n\n const body = requestBody(entry, warn);\n const contributed = contributionsOf(entry.hooks, warn);\n\n return {\n operationId: ids.take(entry),\n ...(entry.def.docs?.summary === undefined\n ? {}\n : { summary: entry.def.docs.summary }),\n ...(entry.def.docs?.description === undefined\n ? {}\n : { description: entry.def.docs.description }),\n ...(entry.def.docs?.tags === undefined\n ? {}\n : { tags: entry.def.docs.tags }),\n ...(entry.def.docs?.deprecated === undefined\n ? {}\n : { deprecated: entry.def.docs.deprecated }),\n ...(parameters.length > 0 ? { parameters } : {}),\n ...(body ? { requestBody: body } : {}),\n ...(contributed.security.length > 0\n ? {\n security: contributed.security.map((requirement) => ({\n [requirement.name]: requirement.scopes ?? [],\n })),\n }\n : {}),\n responses: responses(entry, options, contributed, components, warn),\n };\n}\n\n/**\n * Hands out the `operationId` of every route in one document.\n *\n * Uniqueness is a requirement of the specification and cannot be decided\n * one route at a time: the pair a name is built from — the controller and\n * the field — is not unique on its own. One instance mounted under two\n * prefixes is a documented pattern (`/v1` and `/v2` from the same\n * controller), and two controller classes in different modules may simply\n * share a class name. Either way the pair repeats while the routes differ.\n *\n * A repeat falls back to what the route is named by when it has no field\n * at all — its method and path — because that is derived from the route\n * rather than from the order routes were visited in. A numbered name is\n * the last resort only, since it would move between documents as routes\n * are added, and an `operationId` is what a generated client calls its\n * method.\n */\nexport interface OperationIds {\n /** The id for this route, unique within the document being built. */\n take(entry: RouteTableEntry): string;\n}\n\nexport function operationIds(): OperationIds {\n const taken = new Set<string>();\n\n return {\n take(entry: RouteTableEntry): string {\n const preferred = declaredId(entry) ?? derivedId(entry);\n\n if (!taken.has(preferred)) {\n taken.add(preferred);\n\n return preferred;\n }\n\n const derived = derivedId(entry);\n\n if (!taken.has(derived)) {\n taken.add(derived);\n\n return derived;\n }\n\n let name = derived;\n\n for (let attempt = 2; taken.has(name); attempt += 1) {\n name = `${derived}${attempt}`;\n }\n\n taken.add(name);\n\n return name;\n },\n };\n}\n\n/**\n * The name the controller gave it: the field, qualified by the controller\n * — `usersSetAvatar` for `setAvatar` on `UsersController`.\n */\nfunction declaredId(entry: RouteTableEntry): string | undefined {\n if (entry.name === undefined) {\n return undefined;\n }\n\n const controller = entry.controller.replace(/Controller$/, \"\");\n\n return `${lowerFirst(controller)}${capitalize(entry.name)}`;\n}\n\n/**\n * What identifies a route when its field does not: its method and path.\n *\n * Also the name of a route mounted standalone, which has no field to be\n * called after.\n */\nfunction derivedId(entry: RouteTableEntry): string {\n return `${entry.method.toLowerCase()}${entry.path\n .split(/[/:*]/)\n .filter(Boolean)\n .map(capitalize)\n .join(\"\")}`;\n}\n\n/**\n * Describes the request body, when the route reads one.\n *\n * The media type follows the declaration, not a guess: `\"form\"` accepts\n * both multipart and urlencoded at runtime, so both are listed — unless\n * the schema declares a binary field, which only multipart can carry.\n */\nfunction requestBody(\n entry: RouteTableEntry,\n warn: (message: string) => void,\n): { required: boolean; content: ContentMap } | undefined {\n const declared = entry.def.schema?.body;\n const bodyType = entry.def.bodyType ?? (declared ? \"json\" : undefined);\n\n if (!bodyType) {\n return undefined;\n }\n\n const described = declared\n ? emitted(declared, \"the body\", warn, \"input\")\n : undefined;\n\n const schema = described ? { schema: described } : {};\n\n // Whether a body must be sent is a question about the wire, not about\n // the schema: `json` and `form` parse it before anything is validated,\n // and an empty one fails that parse with a `400` — so the client has to\n // send something whether or not a shape was declared. `text` has no such\n // step, because an absent body reads as `\"\"` just like an empty one; if\n // that string is unacceptable, the schema is where it says so. Neither\n // has `stream`: an absent body is handed over as an empty stream.\n const required = bodyType !== \"text\" && bodyType !== \"stream\";\n\n if (bodyType === \"stream\") {\n /**\n * Bytes with no shape to describe: the route takes the body unread, so\n * the document says what it is rather than what is in it. A `stream`\n * route cannot carry a body schema at all — the compiler refuses the\n * pair — so there is never a `schema` to attach here.\n */\n return { required, content: { \"application/octet-stream\": {} } };\n }\n\n if (bodyType === \"text\") {\n return { required, content: { \"text/plain\": schema } };\n }\n\n if (bodyType === \"json\") {\n return { required, content: { \"application/json\": schema } };\n }\n\n const content: ContentMap = { \"multipart/form-data\": schema };\n\n if (!described || !carriesBinary(described)) {\n content[\"application/x-www-form-urlencoded\"] = schema;\n }\n\n return { required, content };\n}\n\n/**\n * Whether any part of an emitted schema describes binary content.\n *\n * Walked with a stack rather than by recursion, and remembering what it has\n * already seen: the shape comes from an adapter, so its depth is the\n * author's to choose and nothing says it is a tree. Recursion would answer\n * a deep one by overflowing the stack, and a self-referential one by never\n * returning — both while `createApp` is still assembling the application.\n */\nfunction carriesBinary(root: unknown): boolean {\n const pending: unknown[] = [root];\n const seen = new Set<unknown>();\n\n while (pending.length > 0) {\n const node = pending.pop();\n\n if (node === null || typeof node !== \"object\" || seen.has(node)) {\n continue;\n }\n\n seen.add(node);\n\n if ((node as { contentEncoding?: unknown }).contentEncoding === \"binary\") {\n return true;\n }\n\n for (const value of Object.values(node)) {\n pending.push(value);\n }\n }\n\n return false;\n}\n\n/**\n * Describes what the route answers with.\n *\n * The declared responses come first; the framework's own failures fill in\n * around them, and never over them — an author who documented `422`\n * himself has said something more specific than this could.\n */\nfunction responses(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const declared: Record<string, ResponseObject> = {};\n const response = entry.def.schema?.response;\n\n if (response && \"~standard\" in response) {\n declared[\"200\"] = jsonResponse(\n response as AnySchema,\n \"Successful response\",\n warn,\n );\n } else if (response) {\n for (const [status, schema] of Object.entries(response)) {\n declared[status] =\n schema === null\n ? { description: describeStatus(status) }\n : jsonResponse(schema, describeStatus(status), warn);\n }\n }\n\n if (!declared[\"200\"] && !hasSuccess(declared)) {\n declared[\"200\"] = { description: \"Successful response\" };\n }\n\n return {\n ...declared,\n ...frameworkFailures(\n entry,\n options,\n contributed,\n declared,\n components,\n warn,\n ),\n };\n}\n\nfunction frameworkFailures(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n declared: Record<string, ResponseObject>,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const schema = entry.def.schema;\n const failures = new Map<string, Failure[]>();\n\n const add = (status: number, failure: Failure): void => {\n const key = String(status);\n\n failures.set(key, [...(failures.get(key) ?? []), failure]);\n };\n\n /** Defines the envelope of one failure once, and refers to it. */\n const envelopeRef = (\n status: number,\n error?: string,\n message?: string,\n ): Record<string, unknown> =>\n components.ref(\n failureName(status, error),\n envelope(status, error, message),\n ) as unknown as Record<string, unknown>;\n\n if (schema?.params || schema?.query || schema?.headers || schema?.body) {\n add(options.validationStatus, {\n description: \"Request failed schema validation\",\n schema: components.ref(\n failureName(options.validationStatus, \"VALIDATION_FAILED\"),\n validationFailed(options.validationStatus),\n ) as unknown as Record<string, unknown>,\n });\n }\n\n for (const requirement of contributed.security) {\n const status = requirement.status ?? 401;\n\n add(status, {\n description:\n requirement.description ??\n `Request did not satisfy ${requirement.name}`,\n schema: envelopeRef(status, requirement.error, requirement.message),\n });\n }\n\n for (const response of contributed.responses) {\n const described = response.schema\n ? emitted(response.schema, \"a hook's response\", warn)\n : undefined;\n\n add(response.status, {\n description: response.description,\n schema:\n described ??\n envelopeRef(response.status, response.error, response.message),\n });\n }\n\n if (schema?.body || entry.def.bodyType) {\n const unparsable = parseFailure(entry.def.bodyType);\n\n if (unparsable) {\n add(400, {\n description: \"Body could not be parsed in the declared shape\",\n schema: envelopeRef(400, unparsable.error, unparsable.message),\n });\n }\n\n add(413, {\n description: \"Body exceeded the configured size limit\",\n schema: envelopeRef(\n 413,\n \"BODY_TOO_LARGE\",\n \"Body exceeds the configured limit\",\n ),\n });\n }\n\n add(500, {\n description: \"The request failed and nothing mapped the failure\",\n schema: envelopeRef(500, \"INTERNAL_SERVER_ERROR\", \"Internal Server Error\"),\n });\n\n const responses: Record<string, ResponseObject> = {};\n\n for (const [status, list] of failures) {\n responses[status] = merge(declared[status], list);\n }\n\n return responses;\n}\n\n/** One way the framework itself can answer. */\ninterface Failure {\n readonly description: string;\n readonly schema: Record<string, unknown>;\n}\n\n/**\n * Folds everything that answers with one status into one response.\n *\n * Three things can meet on a status: what the route declared, and any\n * number of failures the framework fills in — a rejected schema and an\n * unparsable body collide on `400` as soon as an application configures\n * `validation: { status: 400 }`, and a route that answers `404` itself\n * shares that status with nothing while sharing `400` with the parser.\n *\n * All of them are true at once, so the document says so with an `anyOf`\n * rather than picking a winner. The route's own body comes first: it is\n * the answer the endpoint is about, and the failures are what can happen\n * to it. Identical bodies are folded together — a `$ref` repeated is one\n * alternative, not two.\n */\nfunction merge(\n declared: ResponseObject | undefined,\n list: readonly Failure[],\n): ResponseObject {\n const descriptions = [\n ...(declared ? [declared.description] : []),\n ...list.map((failure) => failure.description),\n ];\n\n const schemas = distinct([\n ...declaredSchemas(declared),\n ...list.map((failure) => failure.schema),\n ]);\n\n const [only] = schemas;\n\n return {\n description: descriptions.join(\"; \"),\n ...(schemas.length === 1 && only\n ? { content: { \"application/json\": { schema: only } } }\n : { content: { \"application/json\": { schema: { anyOf: schemas } } } }),\n };\n}\n\n/**\n * The body a declared response describes, if it describes one: a response\n * whose schema could not be emitted contributes its description alone.\n */\nfunction declaredSchemas(\n declared: ResponseObject | undefined,\n): Record<string, unknown>[] {\n const schema = declared?.content?.[\"application/json\"]?.schema;\n\n return schema ? [schema] : [];\n}\n\nfunction distinct(\n schemas: readonly Record<string, unknown>[],\n): Record<string, unknown>[] {\n const seen = new Set<string>();\n\n return schemas.filter((schema) => {\n const fingerprint = JSON.stringify(schema);\n\n if (seen.has(fingerprint)) {\n return false;\n }\n\n seen.add(fingerprint);\n\n return true;\n });\n}\n\nfunction jsonResponse(\n schema: unknown,\n description: string,\n warn: (message: string) => void,\n): ResponseObject {\n const described = emitted(schema, \"a response\", warn);\n\n if (!described) {\n return { description };\n }\n\n return {\n description,\n content: { \"application/json\": { schema: described } },\n };\n}\n\nfunction hasSuccess(declared: Record<string, ResponseObject>): boolean {\n return Object.keys(declared).some((status) => status.startsWith(\"2\"));\n}\n\nfunction describeStatus(status: string): string {\n if (status === \"204\") {\n return \"No content\";\n }\n\n return status.startsWith(\"2\") ? \"Successful response\" : `Response ${status}`;\n}\n\n/**\n * The body of one framework-produced failure, as JSON Schema.\n *\n * The status is a `const`, and so is the code whenever the framework knows\n * which one it will be: a generated client can then discriminate on\n * `error` instead of receiving \"an object with three strings\", and a\n * reader of the document sees `\"BODY_TOO_LARGE\"` rather than `string`.\n *\n * The message is documented by example, never as a `const` — it is wording\n * meant for a human, the one field a client must not match on, and the one\n * this project reserves the right to reword.\n */\nfunction envelope(\n status: number,\n error?: string,\n message?: string,\n): JsonSchemaObject {\n return {\n type: \"object\",\n required: [\"status\", \"message\", \"error\"],\n properties: {\n status: { type: \"integer\", const: status },\n message: message\n ? { type: \"string\", examples: [message] }\n : { type: \"string\" },\n error: error ? { type: \"string\", const: error } : { type: \"string\" },\n },\n };\n}\n\n/** The validation envelope: the standard three fields, plus the issues. */\nfunction validationFailed(status: number): Record<string, unknown> {\n const base = envelope(status, \"VALIDATION_FAILED\", \"Validation failed\");\n\n return {\n ...base,\n required: [\"status\", \"message\", \"error\", \"issues\"],\n properties: {\n ...(base.properties as Record<string, unknown>),\n issues: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"message\", \"path\"],\n properties: {\n message: { type: \"string\" },\n path: { type: \"array\", items: { type: [\"string\", \"number\"] } },\n },\n },\n },\n },\n };\n}\n\n/**\n * How a body of this type fails to parse — or nothing, for a text or a\n * stream body: one is whatever bytes arrived, the other is handed over\n * unread, and neither can be malformed.\n */\nfunction parseFailure(\n bodyType: BodyType | undefined,\n): { error: string; message: string } | undefined {\n if (bodyType === \"text\" || bodyType === \"stream\") {\n return undefined;\n }\n\n return bodyType === \"form\"\n ? { error: \"MALFORMED_FORM\", message: \"Body is not a valid form\" }\n : { error: \"MALFORMED_JSON\", message: \"Body is not valid JSON\" };\n}\n\nfunction capitalize(value: string): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\nfunction lowerFirst(value: string): string {\n return value.charAt(0).toLowerCase() + value.slice(1);\n}\n",
10
- "/**\n * The documentation endpoints as a mountable controller.\n *\n * `openapi()` returns data and mounts nothing, which is the right default\n * for a generator but a chore in the ordinary case: every application ends\n * up writing the same controller, holding the same application, to serve\n * the same two routes. {@link docs} is that controller, written once.\n *\n * It is a controller like any other — mounted in `routes`, groupable,\n * hookable — that receives the application through `onMount` and generates\n * the document from it at startup.\n *\n * @module\n */\n\nimport type { App, RouteDef, SchemaConfig, ValidatePath } from \"@tetsujs/core\";\nimport { onMount, route } from \"@tetsujs/core\";\nimport type { OpenApiDocument } from \"./document.ts\";\nimport type { GeneratorWarning } from \"./index.ts\";\nimport { openapi } from \"./index.ts\";\nimport type { DocsAssets, DocsUi } from \"./page.ts\";\nimport { docsPage } from \"./page.ts\";\n\n/** What the documentation controller serves, and from where. */\nexport interface DocsOptions<Path extends string, UiPath extends string> {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n\n /** Where the document is served. Defaults to `/openapi.json`. */\n readonly path?: Path & ValidatePath<Path>;\n\n /** Where the page is served. Defaults to `/docs`. */\n readonly uiPath?: UiPath & ValidatePath<UiPath>;\n\n /** Which renderer the page bootstraps. Defaults to `\"scalar\"`. */\n readonly ui?: DocsUi;\n\n /** Browser title of the page. Defaults to the document's own title. */\n readonly title?: string;\n\n /** Overrides the renderer's CDN URLs, for a pinned or self-hosted copy. */\n readonly assets?: DocsAssets;\n\n /**\n * Documents the two endpoints this controller serves.\n *\n * Off by default: a reader who is looking at the document has already\n * found the document, and an operation for \"the page you are reading\"\n * is one a generated client would happily produce a method for.\n */\n readonly documentSelf?: boolean;\n\n /**\n * What to do with what the generator could not describe.\n *\n * Defaults to one `console.warn` per warning, at startup — a document\n * that silently omits a body is worse than a noisy one.\n */\n readonly onWarning?: (warning: GeneratorWarning) => void;\n}\n\n/**\n * The controller {@link docs} returns: two routes and the `onMount` that\n * fills them.\n *\n * Instantiated through the factory, never directly — the routes carry the\n * configured paths in their types, which only the factory can infer.\n */\nexport class DocsController<Path extends string, UiPath extends string> {\n /** The document itself, as `application/json`. */\n readonly json: RouteDef<Path, SchemaConfig, never, undefined, \"GET\">;\n\n /** The page that renders it. */\n readonly ui: RouteDef<UiPath, SchemaConfig, never, undefined, \"GET\">;\n\n private document: OpenApiDocument | undefined;\n\n constructor(\n private readonly options: DocsOptions<Path, UiPath>,\n documentPath: string,\n uiPath: string,\n page: string,\n ) {\n const hidden = !options.documentSelf;\n\n const json = route({\n method: \"GET\",\n path: \"/openapi.json\",\n docs: { summary: \"This document\", tags: [\"docs\"], hidden },\n handler: () => this.document,\n });\n\n const ui = route({\n method: \"GET\",\n path: \"/docs\",\n docs: { summary: \"Rendered documentation\", tags: [\"docs\"], hidden },\n handler: () =>\n new Response(page, {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n }),\n });\n\n /**\n * The configured paths, put back on the routes.\n *\n * `route()` validates its path at the type level, and that check\n * cannot be satisfied by a type variable — `ValidatePath<Path>` stays\n * unresolved while `Path` is generic. The routes are therefore built\n * with the default literals and re-typed here, which is also where the\n * configured path lands at runtime.\n */\n this.json = { ...json, path: documentPath } as unknown as this[\"json\"];\n this.ui = { ...ui, path: uiPath } as unknown as this[\"ui\"];\n }\n\n /** Generates the document of the application this was mounted in. */\n [onMount](app: App): void {\n const generated = openapi(app, {\n info: this.options.info,\n ...(this.options.servers ? { servers: this.options.servers } : {}),\n });\n\n this.document = generated.document;\n\n const report = this.options.onWarning ?? warn;\n\n for (const warning of generated.warnings) {\n report(warning);\n }\n }\n}\n\n/**\n * The documentation of an application, as a controller to mount.\n *\n * @example\n * ```ts\n * createApp({\n * routes: [\n * group(\"/api\", { children: [new UsersController(users)] }),\n * docs({ info: { title: \"Users API\", version: \"1.0.0\" } }),\n * ],\n * });\n * ```\n *\n * The document is generated once, at startup, from the application the\n * controller was mounted in. Its own two routes stay out of it — pass\n * `documentSelf` to put them in.\n *\n * The page loads its renderer from a CDN; see {@link docsPage} for the\n * trade and for how to point it at a self-hosted copy.\n */\nexport function docs<\n const Path extends string = \"/openapi.json\",\n const UiPath extends string = \"/docs\",\n>(options: DocsOptions<Path, UiPath>): DocsController<Path, UiPath> {\n const documentPath = options.path ?? \"/openapi.json\";\n const uiPath = options.uiPath ?? \"/docs\";\n\n const page = docsPage({\n ui: options.ui ?? \"scalar\",\n documentUrl: documentPath,\n title: options.title ?? options.info.title,\n ...(options.assets ? { assets: options.assets } : {}),\n });\n\n return new DocsController(options, documentPath, uiPath, page);\n}\n\nfunction warn(warning: GeneratorWarning): void {\n console.warn(`[openapi] ${warning.route}: ${warning.message}`);\n}\n",
9
+ "/**\n * One route table entry, described as an OpenAPI operation.\n *\n * Bodies and responses live here; parameters are in `parameters.ts`. The\n * error responses a route can produce without the author writing anything\n * — a rejected schema, an unparsable body, a body over the limit — are\n * derived from what the route declares and from the options the\n * application actually runs with.\n *\n * @module\n */\n\nimport type {\n AnySchema,\n AppOptions,\n BodyType,\n RouteTableEntry,\n} from \"@tetsujs/core\";\nimport type { HookContributions, SecurityRequirement } from \"./annotations.ts\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport type { SchemaComponents } from \"./components.ts\";\nimport { failureName } from \"./components.ts\";\nimport type {\n ContentMap,\n OperationObject,\n ResponseObject,\n} from \"./document.ts\";\nimport type { JsonSchemaObject } from \"./emit.ts\";\nimport { emitted } from \"./emit.ts\";\nimport { partParameters, pathParameters } from \"./parameters.ts\";\n\n/** Describes one route. */\nexport function operationOf(\n entry: RouteTableEntry,\n options: AppOptions,\n components: SchemaComponents,\n ids: OperationIds,\n warn: (message: string) => void,\n): OperationObject {\n const schema = entry.def.schema;\n\n const parameters = [\n ...pathParameters(entry.path, schema?.params, warn),\n ...partParameters(schema?.query, \"query\", warn),\n ...partParameters(schema?.headers, \"header\", warn),\n ...partParameters(schema?.cookies, \"cookie\", warn),\n ];\n\n const body = requestBody(entry, warn);\n const contributed = contributionsOf(entry.hooks, warn);\n\n return {\n operationId: ids.take(entry),\n ...(entry.def.docs?.summary === undefined\n ? {}\n : { summary: entry.def.docs.summary }),\n ...(entry.def.docs?.description === undefined\n ? {}\n : { description: entry.def.docs.description }),\n ...(entry.def.docs?.tags === undefined\n ? {}\n : { tags: entry.def.docs.tags }),\n ...(entry.def.docs?.deprecated === undefined\n ? {}\n : { deprecated: entry.def.docs.deprecated }),\n ...(parameters.length > 0 ? { parameters } : {}),\n ...(body ? { requestBody: body } : {}),\n ...(contributed.security.length > 0\n ? { security: securityOf(contributed.conditions) }\n : {}),\n responses: responses(entry, options, contributed, components, warn),\n };\n}\n\n/**\n * The security a route asks for, as OpenAPI spells it.\n *\n * OpenAPI reads the entries of an operation's `security` as alternatives —\n * any one of them authorizes a request — and the schemes inside one entry\n * as all required. A route's conditions are the other way round: every\n * hook runs, so every condition is required, and a condition is satisfied\n * by any one of its alternatives. The entries are therefore the\n * combinations — one alternative from each condition, taken together:\n * a hook accepting a cookie *or* a token, next to a CSRF check, is\n * `[{ cookie, csrf }, { token, csrf }]`. With no alternatives anywhere\n * that is a single entry holding every scheme, which is the usual case.\n *\n * Two alternatives of one scheme in a combination ask for the scopes of\n * both; a combination that comes out twice is listed once.\n */\nfunction securityOf(\n conditions: readonly (readonly SecurityRequirement[])[],\n): Record<string, readonly string[]>[] {\n let combinations: Record<string, readonly string[]>[] = [{}];\n\n for (const alternatives of conditions) {\n combinations = combinations.flatMap((combination) =>\n alternatives.map((requirement) => ({\n ...combination,\n [requirement.name]: joined(\n combination[requirement.name],\n requirement.scopes,\n ),\n })),\n );\n }\n\n const seen = new Set<string>();\n\n return combinations.filter((combination) => {\n const key = JSON.stringify(\n Object.entries(combination)\n .map(([name, scopes]) => [name, [...scopes].sort()])\n .sort(),\n );\n\n if (seen.has(key)) {\n return false;\n }\n\n seen.add(key);\n\n return true;\n });\n}\n\n/** The scopes of a scheme already in a combination, and another's. */\nfunction joined(\n present: readonly string[] | undefined,\n added: readonly string[] | undefined,\n): readonly string[] {\n const scopes = [...(present ?? [])];\n\n for (const scope of added ?? []) {\n if (!scopes.includes(scope)) {\n scopes.push(scope);\n }\n }\n\n return scopes;\n}\n\n/**\n * Hands out the `operationId` of every route in one document.\n *\n * An `operationId` is a contract: a generated client names its methods\n * after them, so an id that moves breaks code nobody reviewing the change\n * sees. The id is therefore always stated or derived from names the\n * author wrote — never from where a route happened to land:\n *\n * | | |\n * | --- | --- |\n * | `docs.operationId` on the route | taken as written |\n * | a route of a named controller | the name and the field — `authRequestCode` |\n * | a route of an unnamed object | the field — `requestCode` |\n * | a route mounted standalone | its method and path — `postAuthCode` |\n *\n * Two routes arriving at one id is an error, not something to route\n * around. Falling back to the method and path, or numbering the second\n * one, would hand one of them an id that changes when a path is edited or\n * a route is added — silently, in someone else's SDK. The error names\n * both routes and the two ways out: a controller name of its own, or an\n * id stated on the route. The core already refuses two controllers of\n * one name; what reaches this is two stated ids, or a stated id meeting a\n * derived one.\n */\nexport interface OperationIds {\n /** The id for this route, unique within the document being built. */\n take(entry: RouteTableEntry): string;\n}\n\nexport function operationIds(): OperationIds {\n const taken = new Map<string, RouteTableEntry>();\n\n return {\n take(entry: RouteTableEntry): string {\n const id = idOf(entry);\n const holder = taken.get(id);\n\n if (holder !== undefined) {\n throw new Error(\n `operationId \"${id}\" is taken by both ${describe(holder)} and ${describe(entry)} — a generated client would get one method for two operations. Give one of them docs.operationId, or its controller a name of its own.`,\n );\n }\n\n taken.set(id, entry);\n\n return id;\n },\n };\n}\n\n/** The id a route asks for, before anything checks it is free. */\nfunction idOf(entry: RouteTableEntry): string {\n const stated = entry.def.docs?.operationId;\n\n if (stated !== undefined) {\n return stated;\n }\n\n if (entry.name === undefined) {\n return derivedId(entry);\n }\n\n if (entry.controller === undefined) {\n return entry.name;\n }\n\n const controller = entry.controller.replace(/Controller$/, \"\");\n\n return `${lowerFirst(controller)}${capitalize(entry.name)}`;\n}\n\n/** How the error above names a route: the one line that finds it. */\nfunction describe(entry: RouteTableEntry): string {\n return `${entry.method} ${entry.path}`;\n}\n\n/**\n * What a route mounted standalone is named by: it has no field and no\n * controller, and its method and path are what it has.\n */\nfunction derivedId(entry: RouteTableEntry): string {\n return `${entry.method.toLowerCase()}${entry.path\n .split(/[/:*]/)\n .filter(Boolean)\n .map(capitalize)\n .join(\"\")}`;\n}\n\n/**\n * Describes the request body, when the route reads one.\n *\n * The media type follows the declaration, not a guess: `\"form\"` accepts\n * both multipart and urlencoded at runtime, so both are listed — unless\n * the schema declares a binary field, which only multipart can carry.\n */\nfunction requestBody(\n entry: RouteTableEntry,\n warn: (message: string) => void,\n): { required: boolean; content: ContentMap } | undefined {\n const declared = entry.def.schema?.body;\n const bodyType = entry.def.bodyType ?? (declared ? \"json\" : undefined);\n\n if (!bodyType) {\n return undefined;\n }\n\n const described = declared\n ? emitted(declared, \"the body\", warn, \"input\")\n : undefined;\n\n const schema = described ? { schema: described } : {};\n\n // Whether a body must be sent is a question about the wire, not about\n // the schema: `json` and `form` parse it before anything is validated,\n // and an empty one fails that parse with a `400` — so the client has to\n // send something whether or not a shape was declared. `text` has no such\n // step, because an absent body reads as `\"\"` just like an empty one; if\n // that string is unacceptable, the schema is where it says so. Neither\n // has `stream`: an absent body is handed over as an empty stream.\n const required = bodyType !== \"text\" && bodyType !== \"stream\";\n\n if (bodyType === \"stream\") {\n /**\n * Bytes with no shape to describe: the route takes the body unread, so\n * the document says what it is rather than what is in it. A `stream`\n * route cannot carry a body schema at all — the compiler refuses the\n * pair — so there is never a `schema` to attach here.\n */\n return { required, content: { \"application/octet-stream\": {} } };\n }\n\n if (bodyType === \"text\") {\n return { required, content: { \"text/plain\": schema } };\n }\n\n if (bodyType === \"json\") {\n return { required, content: { \"application/json\": schema } };\n }\n\n const content: ContentMap = { \"multipart/form-data\": schema };\n\n if (!described || !carriesBinary(described)) {\n content[\"application/x-www-form-urlencoded\"] = schema;\n }\n\n return { required, content };\n}\n\n/**\n * Whether any part of an emitted schema describes binary content.\n *\n * Walked with a stack rather than by recursion, and remembering what it has\n * already seen: the shape comes from an adapter, so its depth is the\n * author's to choose and nothing says it is a tree. Recursion would answer\n * a deep one by overflowing the stack, and a self-referential one by never\n * returning — both while `createApp` is still assembling the application.\n */\nfunction carriesBinary(root: unknown): boolean {\n const pending: unknown[] = [root];\n const seen = new Set<unknown>();\n\n while (pending.length > 0) {\n const node = pending.pop();\n\n if (node === null || typeof node !== \"object\" || seen.has(node)) {\n continue;\n }\n\n seen.add(node);\n\n if ((node as { contentEncoding?: unknown }).contentEncoding === \"binary\") {\n return true;\n }\n\n for (const value of Object.values(node)) {\n pending.push(value);\n }\n }\n\n return false;\n}\n\n/**\n * Describes what the route answers with.\n *\n * The declared responses come first; the framework's own failures fill in\n * around them, and never over them — an author who documented `422`\n * himself has said something more specific than this could.\n */\nfunction responses(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const declared: Record<string, ResponseObject> = {};\n const response = entry.def.schema?.response;\n\n if (response && \"~standard\" in response) {\n declared[\"200\"] = jsonResponse(\n response as AnySchema,\n \"Successful response\",\n warn,\n );\n } else if (response) {\n for (const [status, schema] of Object.entries(response)) {\n declared[status] =\n schema === null\n ? { description: describeStatus(status) }\n : jsonResponse(schema, describeStatus(status), warn);\n }\n }\n\n if (!declared[\"200\"] && !hasSuccess(declared)) {\n declared[\"200\"] = { description: \"Successful response\" };\n }\n\n return {\n ...declared,\n ...frameworkFailures(\n entry,\n options,\n contributed,\n declared,\n components,\n warn,\n ),\n };\n}\n\nfunction frameworkFailures(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n declared: Record<string, ResponseObject>,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const schema = entry.def.schema;\n const failures = new Map<string, Failure[]>();\n\n const add = (status: number, failure: Failure): void => {\n const key = String(status);\n\n failures.set(key, [...(failures.get(key) ?? []), failure]);\n };\n\n /** Defines the envelope of one failure once, and refers to it. */\n const envelopeRef = (\n status: number,\n error?: string,\n message?: string,\n ): Record<string, unknown> =>\n components.ref(\n failureName(status, error),\n envelope(status, error, message),\n ) as unknown as Record<string, unknown>;\n\n if (schema?.params || schema?.query || schema?.headers || schema?.body) {\n add(options.validationStatus, {\n description: \"Request failed schema validation\",\n schema: components.ref(\n failureName(options.validationStatus, \"VALIDATION_FAILED\"),\n validationFailed(options.validationStatus),\n ) as unknown as Record<string, unknown>,\n });\n }\n\n for (const requirement of contributed.security) {\n const status = requirement.status ?? 401;\n\n add(status, {\n description:\n requirement.description ??\n `Request did not satisfy ${requirement.name}`,\n schema: envelopeRef(status, requirement.error, requirement.message),\n });\n }\n\n for (const response of contributed.responses) {\n const described = response.schema\n ? emitted(response.schema, \"a hook's response\", warn)\n : undefined;\n\n add(response.status, {\n description: response.description,\n schema:\n described ??\n envelopeRef(response.status, response.error, response.message),\n });\n }\n\n if (schema?.body || entry.def.bodyType) {\n const unparsable = parseFailure(entry.def.bodyType);\n\n if (unparsable) {\n add(400, {\n description: \"Body could not be parsed in the declared shape\",\n schema: envelopeRef(400, unparsable.error, unparsable.message),\n });\n }\n\n add(413, {\n description: \"Body exceeded the configured size limit\",\n schema: envelopeRef(\n 413,\n \"BODY_TOO_LARGE\",\n \"Body exceeds the configured limit\",\n ),\n });\n }\n\n add(500, {\n description: \"The request failed and nothing mapped the failure\",\n schema: envelopeRef(500, \"INTERNAL_SERVER_ERROR\", \"Internal Server Error\"),\n });\n\n const responses: Record<string, ResponseObject> = {};\n\n for (const [status, list] of failures) {\n responses[status] = merge(declared[status], list);\n }\n\n return responses;\n}\n\n/** One way the framework itself can answer. */\ninterface Failure {\n readonly description: string;\n readonly schema: Record<string, unknown>;\n}\n\n/**\n * Folds everything that answers with one status into one response.\n *\n * Three things can meet on a status: what the route declared, and any\n * number of failures the framework fills in — a rejected schema and an\n * unparsable body collide on `400` as soon as an application configures\n * `validation: { status: 400 }`, and a route that answers `404` itself\n * shares that status with nothing while sharing `400` with the parser.\n *\n * All of them are true at once, so the document says so with an `anyOf`\n * rather than picking a winner. The route's own body comes first: it is\n * the answer the endpoint is about, and the failures are what can happen\n * to it. Identical bodies are folded together — a `$ref` repeated is one\n * alternative, not two.\n */\nfunction merge(\n declared: ResponseObject | undefined,\n list: readonly Failure[],\n): ResponseObject {\n const descriptions = [\n ...(declared ? [declared.description] : []),\n ...list.map((failure) => failure.description),\n ];\n\n const schemas = distinct([\n ...declaredSchemas(declared),\n ...list.map((failure) => failure.schema),\n ]);\n\n const [only] = schemas;\n\n return {\n description: descriptions.join(\"; \"),\n ...(schemas.length === 1 && only\n ? { content: { \"application/json\": { schema: only } } }\n : { content: { \"application/json\": { schema: { anyOf: schemas } } } }),\n };\n}\n\n/**\n * The body a declared response describes, if it describes one: a response\n * whose schema could not be emitted contributes its description alone.\n */\nfunction declaredSchemas(\n declared: ResponseObject | undefined,\n): Record<string, unknown>[] {\n const schema = declared?.content?.[\"application/json\"]?.schema;\n\n return schema ? [schema] : [];\n}\n\nfunction distinct(\n schemas: readonly Record<string, unknown>[],\n): Record<string, unknown>[] {\n const seen = new Set<string>();\n\n return schemas.filter((schema) => {\n const fingerprint = JSON.stringify(schema);\n\n if (seen.has(fingerprint)) {\n return false;\n }\n\n seen.add(fingerprint);\n\n return true;\n });\n}\n\nfunction jsonResponse(\n schema: unknown,\n description: string,\n warn: (message: string) => void,\n): ResponseObject {\n const described = emitted(schema, \"a response\", warn);\n\n if (!described) {\n return { description };\n }\n\n return {\n description,\n content: { \"application/json\": { schema: described } },\n };\n}\n\nfunction hasSuccess(declared: Record<string, ResponseObject>): boolean {\n return Object.keys(declared).some((status) => status.startsWith(\"2\"));\n}\n\nfunction describeStatus(status: string): string {\n if (status === \"204\") {\n return \"No content\";\n }\n\n return status.startsWith(\"2\") ? \"Successful response\" : `Response ${status}`;\n}\n\n/**\n * The body of one framework-produced failure, as JSON Schema.\n *\n * The status is a `const`, and so is the code whenever the framework knows\n * which one it will be: a generated client can then discriminate on\n * `error` instead of receiving \"an object with three strings\", and a\n * reader of the document sees `\"BODY_TOO_LARGE\"` rather than `string`.\n *\n * The message is documented by example, never as a `const` — it is wording\n * meant for a human, the one field a client must not match on, and the one\n * this project reserves the right to reword.\n */\nfunction envelope(\n status: number,\n error?: string,\n message?: string,\n): JsonSchemaObject {\n return {\n type: \"object\",\n required: [\"status\", \"message\", \"error\"],\n properties: {\n status: { type: \"integer\", const: status },\n message: message\n ? { type: \"string\", examples: [message] }\n : { type: \"string\" },\n error: error ? { type: \"string\", const: error } : { type: \"string\" },\n },\n };\n}\n\n/** The validation envelope: the standard three fields, plus the issues. */\nfunction validationFailed(status: number): Record<string, unknown> {\n const base = envelope(status, \"VALIDATION_FAILED\", \"Validation failed\");\n\n return {\n ...base,\n required: [\"status\", \"message\", \"error\", \"issues\"],\n properties: {\n ...(base.properties as Record<string, unknown>),\n issues: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"message\", \"path\"],\n properties: {\n message: { type: \"string\" },\n path: { type: \"array\", items: { type: [\"string\", \"number\"] } },\n },\n },\n },\n },\n };\n}\n\n/**\n * How a body of this type fails to parse — or nothing, for a text or a\n * stream body: one is whatever bytes arrived, the other is handed over\n * unread, and neither can be malformed.\n */\nfunction parseFailure(\n bodyType: BodyType | undefined,\n): { error: string; message: string } | undefined {\n if (bodyType === \"text\" || bodyType === \"stream\") {\n return undefined;\n }\n\n return bodyType === \"form\"\n ? { error: \"MALFORMED_FORM\", message: \"Body is not a valid form\" }\n : { error: \"MALFORMED_JSON\", message: \"Body is not valid JSON\" };\n}\n\nfunction capitalize(value: string): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\nfunction lowerFirst(value: string): string {\n return value.charAt(0).toLowerCase() + value.slice(1);\n}\n",
10
+ "/**\n * The documentation endpoints as a mountable controller.\n *\n * `openapi()` returns data and mounts nothing, which is the right default\n * for a generator but a chore in the ordinary case: every application ends\n * up writing the same controller, holding the same application, to serve\n * the same two routes. {@link docs} is that controller, written once.\n *\n * It is a controller like any other — mounted in `routes`, groupable,\n * hookable — that receives the application through `onMount` and generates\n * the document from it at startup.\n *\n * @module\n */\n\nimport type { App, RouteDef, SchemaConfig, ValidatePath } from \"@tetsujs/core\";\nimport { onMount, route } from \"@tetsujs/core\";\nimport type { OpenApiDocument } from \"./document.ts\";\nimport type { GeneratorWarning } from \"./index.ts\";\nimport { openapi } from \"./index.ts\";\nimport type { DocsAssets, DocsUi } from \"./page.ts\";\nimport { docsPage } from \"./page.ts\";\n\n/** What the documentation controller serves, and from where. */\nexport interface DocsOptions<Path extends string, UiPath extends string> {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n\n /** Where the document is served. Defaults to `/openapi.json`. */\n readonly path?: Path & ValidatePath<Path>;\n\n /** Where the page is served. Defaults to `/docs`. */\n readonly uiPath?: UiPath & ValidatePath<UiPath>;\n\n /** Which renderer the page bootstraps. Defaults to `\"scalar\"`. */\n readonly ui?: DocsUi;\n\n /** Browser title of the page. Defaults to the document's own title. */\n readonly title?: string;\n\n /** Overrides the renderer's CDN URLs, for a pinned or self-hosted copy. */\n readonly assets?: DocsAssets;\n\n /**\n * Documents the two endpoints this controller serves.\n *\n * Off by default: a reader who is looking at the document has already\n * found the document, and an operation for \"the page you are reading\"\n * is one a generated client would happily produce a method for.\n */\n readonly documentSelf?: boolean;\n\n /**\n * What to do with what the generator could not describe.\n *\n * Defaults to one `console.warn` per warning, at startup — a document\n * that silently omits a body is worse than a noisy one.\n */\n readonly onWarning?: (warning: GeneratorWarning) => void;\n}\n\n/**\n * The controller {@link docs} returns: two routes and the `onMount` that\n * fills them.\n *\n * Instantiated through the factory, never directly — the routes carry the\n * configured paths in their types, which only the factory can infer.\n */\nexport class DocsController<Path extends string, UiPath extends string> {\n /** The document itself, as `application/json`. */\n readonly json: RouteDef<Path, SchemaConfig, never, undefined, \"GET\">;\n\n /** The page that renders it. */\n readonly ui: RouteDef<UiPath, SchemaConfig, never, undefined, \"GET\">;\n\n private document: OpenApiDocument | undefined;\n\n constructor(\n private readonly options: DocsOptions<Path, UiPath>,\n documentPath: string,\n uiPath: string,\n page: string,\n ) {\n const hidden = !options.documentSelf;\n\n const json = route({\n method: \"GET\",\n path: \"/openapi.json\",\n docs: { summary: \"This document\", tags: [\"docs\"], hidden },\n handler: () => this.document,\n });\n\n const ui = route({\n method: \"GET\",\n path: \"/docs\",\n docs: { summary: \"Rendered documentation\", tags: [\"docs\"], hidden },\n handler: () =>\n new Response(page, {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n }),\n });\n\n /**\n * The configured paths, put back on the routes.\n *\n * `route()` validates its path at the type level, and that check\n * cannot be satisfied by a type variable — `ValidatePath<Path>` stays\n * unresolved while `Path` is generic. The routes are therefore built\n * with the default literals and re-typed here, which is also where the\n * configured path lands at runtime.\n */\n this.json = { ...json, path: documentPath } as unknown as this[\"json\"];\n this.ui = { ...ui, path: uiPath } as unknown as this[\"ui\"];\n }\n\n /** Generates the document of the application this was mounted in. */\n [onMount](app: App): void {\n const generated = openapi(app, {\n info: this.options.info,\n ...(this.options.servers ? { servers: this.options.servers } : {}),\n });\n\n this.document = generated.document;\n\n const report = this.options.onWarning ?? warn;\n\n for (const warning of generated.warnings) {\n report(warning);\n }\n }\n}\n\n/**\n * The documentation of an application, as a controller to mount.\n *\n * @example\n * ```ts\n * createApp({\n * routes: [\n * group(\"/api\", { children: [usersController({ users })] }),\n * docs({ info: { title: \"Users API\", version: \"1.0.0\" } }),\n * ],\n * });\n * ```\n *\n * The document is generated once, at startup, from the application the\n * controller was mounted in. Its own two routes stay out of it — pass\n * `documentSelf` to put them in.\n *\n * The page loads its renderer from a CDN; see {@link docsPage} for the\n * trade and for how to point it at a self-hosted copy.\n */\nexport function docs<\n const Path extends string = \"/openapi.json\",\n const UiPath extends string = \"/docs\",\n>(options: DocsOptions<Path, UiPath>): DocsController<Path, UiPath> {\n const documentPath = options.path ?? \"/openapi.json\";\n const uiPath = options.uiPath ?? \"/docs\";\n\n const page = docsPage({\n ui: options.ui ?? \"scalar\",\n documentUrl: documentPath,\n title: options.title ?? options.info.title,\n ...(options.assets ? { assets: options.assets } : {}),\n });\n\n return new DocsController(options, documentPath, uiPath, page);\n}\n\nfunction warn(warning: GeneratorWarning): void {\n console.warn(`[openapi] ${warning.route}: ${warning.message}`);\n}\n",
11
11
  "/**\n * The HTML page that renders a document.\n *\n * Returns a string and mounts nothing: which path serves the page, and\n * whether one is served at all, is the application's decision like every\n * other route.\n *\n * The renderers themselves are loaded from a CDN. Vendoring Scalar or\n * Swagger UI would put megabytes of somebody else's bundle into a\n * dependency-free repository, so the trade is stated instead of hidden:\n * the documentation page — and only it — needs the network, and the URL is\n * a parameter, so a self-hosted copy is one option away. Nothing is\n * fetched unless this function is called.\n *\n * @module\n */\n\n/** The renderers this package knows how to bootstrap. */\nexport type DocsUi = \"scalar\" | \"swagger-ui\" | \"redoc\";\n\n/** Where a renderer is loaded from. */\nexport interface DocsAssets {\n readonly script: string;\n readonly style?: string;\n}\n\n/** What the page needs to know. */\nexport interface DocsPageOptions {\n /** Which renderer to bootstrap. */\n readonly ui: DocsUi;\n\n /** Where the document is served — the page fetches it from there. */\n readonly documentUrl: string;\n\n /** Browser title; defaults to `\"API documentation\"`. */\n readonly title?: string;\n\n /** Overrides the CDN URLs, for a pinned version or a self-hosted copy. */\n readonly assets?: DocsAssets;\n}\n\nconst defaults: Record<DocsUi, DocsAssets> = {\n scalar: {\n script: \"https://cdn.jsdelivr.net/npm/@scalar/api-reference\",\n },\n \"swagger-ui\": {\n script:\n \"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js\",\n style: \"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css\",\n },\n redoc: {\n script: \"https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js\",\n },\n};\n\n/**\n * Builds the documentation page.\n *\n * Called by `docs()` for the page it serves; call it directly to serve one\n * from somewhere else.\n *\n * @example\n * ```ts\n * const page = docsPage({ ui: \"scalar\", documentUrl: \"/openapi.json\" });\n *\n * const ui = route({\n * method: \"GET\",\n * path: \"/docs\",\n * handler: () =>\n * new Response(page, { headers: { \"content-type\": \"text/html\" } }),\n * });\n * ```\n */\nexport function docsPage(options: DocsPageOptions): string {\n const assets = options.assets ?? defaults[options.ui];\n const title = escapeHtml(options.title ?? \"API documentation\");\n const url = escapeHtml(options.documentUrl);\n\n return [\n \"<!doctype html>\",\n '<html lang=\"en\">',\n \"<head>\",\n '<meta charset=\"utf-8\" />',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n `<title>${title}</title>`,\n assets.style\n ? `<link rel=\"stylesheet\" href=\"${escapeHtml(assets.style)}\" />`\n : \"\",\n \"</head>\",\n \"<body>\",\n body(options.ui, url),\n `<script src=\"${escapeHtml(assets.script)}\"></script>`,\n bootstrap(options.ui, url),\n \"</body>\",\n \"</html>\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nfunction body(ui: DocsUi, url: string): string {\n if (ui === \"scalar\") {\n return `<script id=\"api-reference\" data-url=\"${url}\"></script>`;\n }\n\n if (ui === \"redoc\") {\n return `<redoc spec-url=\"${url}\"></redoc>`;\n }\n\n return '<div id=\"swagger-ui\"></div>';\n}\n\nfunction bootstrap(ui: DocsUi, url: string): string {\n if (ui !== \"swagger-ui\") {\n return \"\";\n }\n\n return [\n \"<script>\",\n `SwaggerUIBundle({ url: \"${url}\", dom_id: \"#swagger-ui\" });`,\n \"</script>\",\n ].join(\"\\n\");\n}\n\n/**\n * Escapes the values that reach the markup.\n *\n * The document URL and the title come from the application, but they are\n * still values interpolated into HTML — a quote in either would break out\n * of the attribute it sits in.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#39;\");\n}\n",
12
- "/**\n * OpenAPI generation for the framework.\n *\n * Reads a compiled application — `app.entries` and the options it actually\n * runs with — and returns an OpenAPI 3.1 document. Nothing is registered,\n * served or mutated: the document is data, and serving it is a route the\n * application declares like any other.\n *\n * Serving it is a route the application declares — or `docs()`, the\n * controller that declares those routes for you.\n *\n * @example\n * ```ts\n * const { document, warnings } = openapi(app, {\n * info: { title: \"Users API\", version: \"1.0.0\" },\n * });\n * ```\n *\n * @module\n */\n\nimport type { App } from \"@tetsujs/core\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport { schemaComponents } from \"./components.ts\";\nimport type { OpenApiDocument, PathItemObject } from \"./document.ts\";\nimport { operationIds, operationOf } from \"./operation.ts\";\nimport { toTemplate } from \"./parameters.ts\";\n\nexport type {\n DocumentedResponse,\n HookContributions,\n HookDocs,\n SecurityRequirement,\n SecurityScheme,\n} from \"./annotations.ts\";\nexport { documented, secured } from \"./annotations.ts\";\nexport type { DocsOptions } from \"./controller.ts\";\nexport { DocsController, docs } from \"./controller.ts\";\nexport type {\n ContentMap,\n DocumentInfo,\n DocumentServer,\n OpenApiDocument,\n OperationObject,\n ParameterObject,\n PathItemObject,\n ResponseObject,\n} from \"./document.ts\";\nexport type { DocsAssets, DocsPageOptions, DocsUi } from \"./page.ts\";\nexport { docsPage } from \"./page.ts\";\n\n/**\n * What the generator cannot read off the application.\n */\nexport interface OpenApiOptions {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n}\n\n/**\n * What the generator could not describe faithfully.\n *\n * Each entry names a route and what was missing — almost always a schema\n * from a validator that emits no JSON Schema. Documentation that silently\n * omits a body is worse than documentation that says it does not know, so\n * these are returned rather than swallowed.\n */\nexport interface GeneratorWarning {\n readonly route: string;\n readonly message: string;\n}\n\n/** A generated document together with what could not be described. */\nexport interface GeneratorResult {\n readonly document: OpenApiDocument;\n readonly warnings: readonly GeneratorWarning[];\n}\n\n/**\n * Generates the OpenAPI document of an application.\n *\n * Socket endpoints are skipped: a handshake is a `GET` in the table, but\n * OpenAPI has no way to describe what happens after it, and documenting\n * the upgrade as an ordinary operation would describe a request that\n * clients must not make. AsyncAPI is that format, and a separate document.\n *\n * So are routes marked `docs: { hidden: true }`, and everything they would\n * have contributed with them: a security scheme only a hidden route\n * requires has nothing left to describe.\n *\n * Every route of the table becomes an operation under its path template,\n * with the parameters, body and responses its schemas describe. What the\n * framework itself can answer — a validation failure, an unparsable body,\n * a body over the limit — is filled in from the options the application\n * runs with, so the document states the status actually configured.\n */\nexport function openapi(app: App, options: OpenApiOptions): GeneratorResult {\n const paths: Record<string, PathItemObject> = {};\n const warnings: GeneratorWarning[] = [];\n const schemes: Record<string, unknown> = {};\n const components = schemaComponents();\n const ids = operationIds();\n\n // Which route already occupies each slot of the document. Two routes the\n // router keeps apart can still meet here, because a template says less\n // than a path does: `/files/*` and `/files/:wildcard` both become\n // `/files/{wildcard}`, and a literal `/users/{id}` — which Bun matches as\n // the string it is — is spelled the same as `/users/:id`. The second one\n // then replaces the first, and the document quietly loses an endpoint\n // that works. It still loses it; it no longer does so without saying.\n const occupied = new Map<string, string>();\n\n for (const entry of app.entries) {\n if (entry.ws || entry.def.docs?.hidden) {\n continue;\n }\n\n const template = toTemplate(entry.path);\n const route = `${entry.method} ${entry.path}`;\n\n const operation = operationOf(\n entry,\n app.options,\n components,\n ids,\n (message) => {\n warnings.push({ route, message });\n },\n );\n\n // First definition wins, as it does among the hooks of one route: a\n // name is the identity of a scheme, and the operations that reference\n // it say only the name. A second, different scheme under that name\n // cannot be described without renaming it out from under everything\n // already pointing at it, so it is refused rather than silently\n // swapped in — which used to make every earlier operation describe an\n // authentication its route does not perform.\n for (const requirement of contributionsOf(entry.hooks).security) {\n const defined = schemes[requirement.name];\n\n if (defined === undefined) {\n schemes[requirement.name] = requirement.scheme;\n\n continue;\n }\n\n if (JSON.stringify(defined) !== JSON.stringify(requirement.scheme)) {\n warnings.push({\n route,\n message: `the security scheme \"${requirement.name}\" is already defined differently; this definition is ignored`,\n });\n }\n }\n\n const slot = `${entry.method.toLowerCase()} ${template}`;\n const taken = occupied.get(slot);\n\n if (taken) {\n warnings.push({\n route,\n message: `${template} is already described by ${taken}, whose operation this replaces`,\n });\n }\n\n occupied.set(slot, route);\n\n paths[template] = {\n ...paths[template],\n [entry.method.toLowerCase()]: operation,\n };\n }\n\n return {\n document: {\n openapi: \"3.1.0\",\n info: options.info,\n ...(options.servers ? { servers: options.servers } : {}),\n paths,\n ...componentsOf(schemes, components.schemas),\n },\n warnings,\n };\n}\n\n/**\n * The `components` block, present only when something goes in it: the\n * security schemes the guards registered, and the failure envelopes the\n * operations refer to.\n */\nfunction componentsOf(\n securitySchemes: Record<string, unknown>,\n schemas: Record<string, unknown>,\n): { components?: Record<string, unknown> } {\n const components = {\n ...(Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {}),\n ...(Object.keys(schemas).length > 0 ? { schemas } : {}),\n };\n\n return Object.keys(components).length > 0 ? { components } : {};\n}\n"
12
+ "/**\n * OpenAPI generation for the framework.\n *\n * Reads a compiled application — `app.entries` and the options it actually\n * runs with — and returns an OpenAPI 3.1 document. Nothing is registered,\n * served or mutated: the document is data, and serving it is a route the\n * application declares like any other.\n *\n * Serving it is a route the application declares — or `docs()`, the\n * controller that declares those routes for you.\n *\n * @example\n * ```ts\n * const { document, warnings } = openapi(app, {\n * info: { title: \"Users API\", version: \"1.0.0\" },\n * });\n * ```\n *\n * @module\n */\n\nimport type { App } from \"@tetsujs/core\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport { schemaComponents } from \"./components.ts\";\nimport type { OpenApiDocument, PathItemObject } from \"./document.ts\";\nimport { operationIds, operationOf } from \"./operation.ts\";\nimport { toTemplate } from \"./parameters.ts\";\n\nexport type {\n DocumentedResponse,\n HookContributions,\n HookDocs,\n SecurityAlternatives,\n SecurityRequirement,\n SecurityScheme,\n} from \"./annotations.ts\";\nexport { documented, secured } from \"./annotations.ts\";\nexport type { DocsOptions } from \"./controller.ts\";\nexport { DocsController, docs } from \"./controller.ts\";\nexport type {\n ContentMap,\n DocumentInfo,\n DocumentServer,\n OpenApiDocument,\n OperationObject,\n ParameterObject,\n PathItemObject,\n ResponseObject,\n} from \"./document.ts\";\nexport type { DocsAssets, DocsPageOptions, DocsUi } from \"./page.ts\";\nexport { docsPage } from \"./page.ts\";\n\n/**\n * What the generator cannot read off the application.\n */\nexport interface OpenApiOptions {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n}\n\n/**\n * What the generator could not describe faithfully.\n *\n * Each entry names a route and what was missing — almost always a schema\n * from a validator that emits no JSON Schema. Documentation that silently\n * omits a body is worse than documentation that says it does not know, so\n * these are returned rather than swallowed.\n */\nexport interface GeneratorWarning {\n readonly route: string;\n readonly message: string;\n}\n\n/** A generated document together with what could not be described. */\nexport interface GeneratorResult {\n readonly document: OpenApiDocument;\n readonly warnings: readonly GeneratorWarning[];\n}\n\n/**\n * Generates the OpenAPI document of an application.\n *\n * Socket endpoints are skipped: a handshake is a `GET` in the table, but\n * OpenAPI has no way to describe what happens after it, and documenting\n * the upgrade as an ordinary operation would describe a request that\n * clients must not make. AsyncAPI is that format, and a separate document.\n *\n * So are routes marked `docs: { hidden: true }`, and everything they would\n * have contributed with them: a security scheme only a hidden route\n * requires has nothing left to describe.\n *\n * Every route of the table becomes an operation under its path template,\n * with the parameters, body and responses its schemas describe. What the\n * framework itself can answer — a validation failure, an unparsable body,\n * a body over the limit — is filled in from the options the application\n * runs with, so the document states the status actually configured.\n */\nexport function openapi(app: App, options: OpenApiOptions): GeneratorResult {\n const paths: Record<string, PathItemObject> = {};\n const warnings: GeneratorWarning[] = [];\n const schemes: Record<string, unknown> = {};\n const components = schemaComponents();\n const ids = operationIds();\n\n // Which route already occupies each slot of the document. Two routes the\n // router keeps apart can still meet here, because a template says less\n // than a path does: `/files/*` and `/files/:wildcard` both become\n // `/files/{wildcard}`, and a literal `/users/{id}` — which Bun matches as\n // the string it is — is spelled the same as `/users/:id`. The second one\n // then replaces the first, and the document quietly loses an endpoint\n // that works. It still loses it; it no longer does so without saying.\n const occupied = new Map<string, string>();\n\n for (const entry of app.entries) {\n if (entry.ws || entry.def.docs?.hidden) {\n continue;\n }\n\n const template = toTemplate(entry.path);\n const route = `${entry.method} ${entry.path}`;\n\n const operation = operationOf(\n entry,\n app.options,\n components,\n ids,\n (message) => {\n warnings.push({ route, message });\n },\n );\n\n // First definition wins, as it does among the hooks of one route: a\n // name is the identity of a scheme, and the operations that reference\n // it say only the name. A second, different scheme under that name\n // cannot be described without renaming it out from under everything\n // already pointing at it, so it is refused rather than silently\n // swapped in — which used to make every earlier operation describe an\n // authentication its route does not perform.\n for (const requirement of contributionsOf(entry.hooks).security) {\n const defined = schemes[requirement.name];\n\n if (defined === undefined) {\n schemes[requirement.name] = requirement.scheme;\n\n continue;\n }\n\n if (JSON.stringify(defined) !== JSON.stringify(requirement.scheme)) {\n warnings.push({\n route,\n message: `the security scheme \"${requirement.name}\" is already defined differently; this definition is ignored`,\n });\n }\n }\n\n const slot = `${entry.method.toLowerCase()} ${template}`;\n const taken = occupied.get(slot);\n\n if (taken) {\n warnings.push({\n route,\n message: `${template} is already described by ${taken}, whose operation this replaces`,\n });\n }\n\n occupied.set(slot, route);\n\n paths[template] = {\n ...paths[template],\n [entry.method.toLowerCase()]: operation,\n };\n }\n\n return {\n document: {\n openapi: \"3.1.0\",\n info: options.info,\n ...(options.servers ? { servers: options.servers } : {}),\n paths,\n ...componentsOf(schemes, components.schemas),\n },\n warnings,\n };\n}\n\n/**\n * The `components` block, present only when something goes in it: the\n * security schemes the guards registered, and the failure envelopes the\n * operations refer to.\n */\nfunction componentsOf(\n securitySchemes: Record<string, unknown>,\n schemas: Record<string, unknown>,\n): { components?: Record<string, unknown> } {\n const components = {\n ...(Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {}),\n ...(Object.keys(schemas).length > 0 ? { schemas } : {}),\n };\n\n return Object.keys(components).length > 0 ? { components } : {};\n}\n"
13
13
  ],
14
- "mappings": "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",
15
- "debugId": "698CDE5A6513C23964756E2164756E21",
14
+ "mappings": "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",
15
+ "debugId": "AFF7EFC95370A0C664756E2164756E21",
16
16
  "names": []
17
17
  }
@@ -17,19 +17,26 @@ export declare function operationOf(entry: RouteTableEntry, options: AppOptions,
17
17
  /**
18
18
  * Hands out the `operationId` of every route in one document.
19
19
  *
20
- * Uniqueness is a requirement of the specification and cannot be decided
21
- * one route at a time: the pair a name is built from — the controller and
22
- * the field — is not unique on its own. One instance mounted under two
23
- * prefixes is a documented pattern (`/v1` and `/v2` from the same
24
- * controller), and two controller classes in different modules may simply
25
- * share a class name. Either way the pair repeats while the routes differ.
20
+ * An `operationId` is a contract: a generated client names its methods
21
+ * after them, so an id that moves breaks code nobody reviewing the change
22
+ * sees. The id is therefore always stated or derived from names the
23
+ * author wrote — never from where a route happened to land:
26
24
  *
27
- * A repeat falls back to what the route is named by when it has no field
28
- * at all — its method and path — because that is derived from the route
29
- * rather than from the order routes were visited in. A numbered name is
30
- * the last resort only, since it would move between documents as routes
31
- * are added, and an `operationId` is what a generated client calls its
32
- * method.
25
+ * | | |
26
+ * | --- | --- |
27
+ * | `docs.operationId` on the route | taken as written |
28
+ * | a route of a named controller | the name and the field — `authRequestCode` |
29
+ * | a route of an unnamed object | the field — `requestCode` |
30
+ * | a route mounted standalone | its method and path — `postAuthCode` |
31
+ *
32
+ * Two routes arriving at one id is an error, not something to route
33
+ * around. Falling back to the method and path, or numbering the second
34
+ * one, would hand one of them an id that changes when a path is edited or
35
+ * a route is added — silently, in someone else's SDK. The error names
36
+ * both routes and the two ways out: a controller name of its own, or an
37
+ * id stated on the route. The core already refuses two controllers of
38
+ * one name; what reaches this is two stated ids, or a stated id meeting a
39
+ * derived one.
33
40
  */
34
41
  export interface OperationIds {
35
42
  /** The id for this route, unique within the document being built. */
@@ -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,CAsCjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,YAAY,IAAI,YAAY,CAgC3C"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tetsujs/openapi",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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.3.0"
37
+ "@tetsujs/core": "^0.4.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@tetsujs/core": "0.3.0"
40
+ "@tetsujs/core": "0.4.1"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"
@@ -21,12 +21,12 @@ import type { AnyHook, AnySchema } from "@tetsujs/core";
21
21
  /**
22
22
  * The definition of a scheme, as OpenAPI spells it.
23
23
  *
24
- * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2` and
25
- * `openIdConnect` carry different fields, and narrowing them here would
26
- * be a second, poorer copy of the specification.
24
+ * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2`,
25
+ * `openIdConnect` and `mutualTLS` carry different fields, and narrowing
26
+ * them here would be a second, poorer copy of the specification.
27
27
  */
28
28
  export interface SecurityScheme {
29
- readonly type: "http" | "apiKey" | "oauth2" | "openIdConnect";
29
+ readonly type: "http" | "apiKey" | "oauth2" | "openIdConnect" | "mutualTLS";
30
30
  readonly [key: string]: unknown;
31
31
  }
32
32
 
@@ -85,10 +85,26 @@ export interface DocumentedResponse {
85
85
  readonly message?: string;
86
86
  }
87
87
 
88
+ /**
89
+ * Requirements of which a hook accepts any one: a session cookie *or* a
90
+ * bearer token, checked by one hook.
91
+ *
92
+ * "Either" lives inside a hook, never between hooks — every hook of a
93
+ * route runs, so the schemes of two hooks are both required. A hook that
94
+ * takes either of two credentials says so here, and the document lists
95
+ * the combinations a client may bring.
96
+ */
97
+ export interface SecurityAlternatives {
98
+ readonly anyOf: readonly SecurityRequirement[];
99
+ }
100
+
88
101
  /** Everything a hook tells the generator about itself. */
89
102
  export interface HookDocs {
90
- /** What the hook demands of a request. */
91
- readonly security?: SecurityRequirement;
103
+ /**
104
+ * What the hook demands of a request: one requirement, or alternatives
105
+ * of which any one will do.
106
+ */
107
+ readonly security?: SecurityRequirement | SecurityAlternatives;
92
108
 
93
109
  /** What the hook can answer with. */
94
110
  readonly responses?: readonly DocumentedResponse[];
@@ -128,6 +144,10 @@ export function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {
128
144
  * Annotates a hook with the security it enforces — {@link documented} for
129
145
  * the case that comes up most.
130
146
  *
147
+ * One requirement, or `{ anyOf: [...] }` for a hook that accepts any one
148
+ * of several: the schemes of different hooks are all required, the
149
+ * alternatives of one hook are not.
150
+ *
131
151
  * @example
132
152
  * ```ts
133
153
  * export const auth = secured(
@@ -142,10 +162,22 @@ export function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {
142
162
  * },
143
163
  * );
144
164
  * ```
165
+ *
166
+ * @example A hook that accepts a session cookie or a bearer token
167
+ * ```ts
168
+ * export const caller = secured(
169
+ * hook.beforeParse((ctx) => {
170
+ * const user = sessions.fromCookie(ctx) ?? tokens.fromHeader(ctx);
171
+ * if (!user) throw new HttpError(401);
172
+ * return { user };
173
+ * }),
174
+ * { anyOf: [cookieSession, bearerToken] },
175
+ * );
176
+ * ```
145
177
  */
146
178
  export function secured<H extends AnyHook>(
147
179
  hook: H,
148
- requirement: SecurityRequirement,
180
+ requirement: SecurityRequirement | SecurityAlternatives,
149
181
  ): H {
150
182
  return documented(hook, { security: requirement });
151
183
  }
@@ -155,14 +187,28 @@ export function docsOf(hook: AnyHook): HookDocs | undefined {
155
187
  return (hook as unknown as Record<string, HookDocs | undefined>)[docsKey];
156
188
  }
157
189
 
158
- /** Reads the security requirement a hook carries, if it carries one. */
159
- export function securityOf(hook: AnyHook): SecurityRequirement | undefined {
190
+ /** Reads the security a hook was annotated with, if it was. */
191
+ export function securityOf(
192
+ hook: AnyHook,
193
+ ): SecurityRequirement | SecurityAlternatives | undefined {
160
194
  return docsOf(hook)?.security;
161
195
  }
162
196
 
163
197
  /** What the hooks of one route contribute, in execution order. */
164
198
  export interface HookContributions {
199
+ /**
200
+ * Every scheme the hooks mention, once per name, with the scopes of all
201
+ * of them: what the document registers, and what a refusal answers with.
202
+ */
165
203
  readonly security: readonly SecurityRequirement[];
204
+
205
+ /**
206
+ * What a request must satisfy, one condition per hook: every condition
207
+ * is required, and any one requirement of a condition satisfies it. A
208
+ * hook with one requirement is a condition of one.
209
+ */
210
+ readonly conditions: readonly (readonly SecurityRequirement[])[];
211
+
166
212
  readonly responses: readonly DocumentedResponse[];
167
213
  }
168
214
 
@@ -171,7 +217,9 @@ export interface HookContributions {
171
217
  *
172
218
  * A scheme is registered once per name and a response once per status and
173
219
  * description: the same guard on the application and on a group is one
174
- * requirement, not two.
220
+ * requirement, not two. Two hooks of one scheme asking for different
221
+ * scopes both run, so the requirement asks for the scopes of both — each
222
+ * once, in the order they were first asked for.
175
223
  *
176
224
  * Deduplicating by name means two hooks claiming one name with different
177
225
  * schemes lose one of them, and that is not a duplicate being collapsed —
@@ -183,6 +231,7 @@ export function contributionsOf(
183
231
  warn?: (message: string) => void,
184
232
  ): HookContributions {
185
233
  const security = new Map<string, SecurityRequirement>();
234
+ const conditions = new Map<string, readonly SecurityRequirement[]>();
186
235
  const responses = new Map<string, DocumentedResponse>();
187
236
 
188
237
  for (const slot of Object.values(chains)) {
@@ -194,17 +243,28 @@ export function contributionsOf(
194
243
  }
195
244
 
196
245
  if (docs.security) {
197
- const claimed = security.get(docs.security.name);
198
-
199
- if (!claimed) {
200
- security.set(docs.security.name, docs.security);
201
- } else if (
202
- JSON.stringify(claimed.scheme) !==
203
- JSON.stringify(docs.security.scheme)
204
- ) {
205
- warn?.(
206
- `two hooks claim the security scheme "${docs.security.name}" with different definitions; one of them is not described`,
207
- );
246
+ const alternatives = alternativesOf(docs.security);
247
+
248
+ conditions.set(conditionKey(alternatives), alternatives);
249
+
250
+ for (const requirement of alternatives) {
251
+ const claimed = security.get(requirement.name);
252
+
253
+ if (!claimed) {
254
+ security.set(requirement.name, requirement);
255
+ } else if (
256
+ JSON.stringify(claimed.scheme) ===
257
+ JSON.stringify(requirement.scheme)
258
+ ) {
259
+ security.set(requirement.name, {
260
+ ...claimed,
261
+ scopes: joinScopes(claimed.scopes, requirement.scopes),
262
+ });
263
+ } else {
264
+ warn?.(
265
+ `two hooks claim the security scheme "${requirement.name}" with different definitions; one of them is not described`,
266
+ );
267
+ }
208
268
  }
209
269
  }
210
270
 
@@ -220,6 +280,44 @@ export function contributionsOf(
220
280
 
221
281
  return {
222
282
  security: [...security.values()],
283
+ conditions: [...conditions.values()],
223
284
  responses: [...responses.values()],
224
285
  };
225
286
  }
287
+
288
+ /** A hook's security as the alternatives it accepts; one is one. */
289
+ function alternativesOf(
290
+ security: SecurityRequirement | SecurityAlternatives,
291
+ ): readonly SecurityRequirement[] {
292
+ return "anyOf" in security ? security.anyOf : [security];
293
+ }
294
+
295
+ /**
296
+ * What makes two conditions the same one: the same guard mounted on the
297
+ * application and on a group is one condition, and counting it twice would
298
+ * multiply its alternatives with themselves.
299
+ */
300
+ function conditionKey(alternatives: readonly SecurityRequirement[]): string {
301
+ return JSON.stringify(
302
+ alternatives.map((requirement) => [
303
+ requirement.name,
304
+ [...(requirement.scopes ?? [])].sort(),
305
+ ]),
306
+ );
307
+ }
308
+
309
+ /** The scopes of two requirements of one scheme, each once, in order. */
310
+ function joinScopes(
311
+ first: readonly string[] | undefined,
312
+ second: readonly string[] | undefined,
313
+ ): readonly string[] {
314
+ const joined = [...(first ?? [])];
315
+
316
+ for (const scope of second ?? []) {
317
+ if (!joined.includes(scope)) {
318
+ joined.push(scope);
319
+ }
320
+ }
321
+
322
+ return joined;
323
+ }
package/src/controller.ts CHANGED
@@ -140,7 +140,7 @@ export class DocsController<Path extends string, UiPath extends string> {
140
140
  * ```ts
141
141
  * createApp({
142
142
  * routes: [
143
- * group("/api", { children: [new UsersController(users)] }),
143
+ * group("/api", { children: [usersController({ users })] }),
144
144
  * docs({ info: { title: "Users API", version: "1.0.0" } }),
145
145
  * ],
146
146
  * });
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export type {
30
30
  DocumentedResponse,
31
31
  HookContributions,
32
32
  HookDocs,
33
+ SecurityAlternatives,
33
34
  SecurityRequirement,
34
35
  SecurityScheme,
35
36
  } from "./annotations.ts";
package/src/operation.ts CHANGED
@@ -16,7 +16,7 @@ import type {
16
16
  BodyType,
17
17
  RouteTableEntry,
18
18
  } from "@tetsujs/core";
19
- import type { HookContributions } from "./annotations.ts";
19
+ import type { HookContributions, SecurityRequirement } from "./annotations.ts";
20
20
  import { contributionsOf } from "./annotations.ts";
21
21
  import type { SchemaComponents } from "./components.ts";
22
22
  import { failureName } from "./components.ts";
@@ -66,32 +66,103 @@ export function operationOf(
66
66
  ...(parameters.length > 0 ? { parameters } : {}),
67
67
  ...(body ? { requestBody: body } : {}),
68
68
  ...(contributed.security.length > 0
69
- ? {
70
- security: contributed.security.map((requirement) => ({
71
- [requirement.name]: requirement.scopes ?? [],
72
- })),
73
- }
69
+ ? { security: securityOf(contributed.conditions) }
74
70
  : {}),
75
71
  responses: responses(entry, options, contributed, components, warn),
76
72
  };
77
73
  }
78
74
 
75
+ /**
76
+ * The security a route asks for, as OpenAPI spells it.
77
+ *
78
+ * OpenAPI reads the entries of an operation's `security` as alternatives —
79
+ * any one of them authorizes a request — and the schemes inside one entry
80
+ * as all required. A route's conditions are the other way round: every
81
+ * hook runs, so every condition is required, and a condition is satisfied
82
+ * by any one of its alternatives. The entries are therefore the
83
+ * combinations — one alternative from each condition, taken together:
84
+ * a hook accepting a cookie *or* a token, next to a CSRF check, is
85
+ * `[{ cookie, csrf }, { token, csrf }]`. With no alternatives anywhere
86
+ * that is a single entry holding every scheme, which is the usual case.
87
+ *
88
+ * Two alternatives of one scheme in a combination ask for the scopes of
89
+ * both; a combination that comes out twice is listed once.
90
+ */
91
+ function securityOf(
92
+ conditions: readonly (readonly SecurityRequirement[])[],
93
+ ): Record<string, readonly string[]>[] {
94
+ let combinations: Record<string, readonly string[]>[] = [{}];
95
+
96
+ for (const alternatives of conditions) {
97
+ combinations = combinations.flatMap((combination) =>
98
+ alternatives.map((requirement) => ({
99
+ ...combination,
100
+ [requirement.name]: joined(
101
+ combination[requirement.name],
102
+ requirement.scopes,
103
+ ),
104
+ })),
105
+ );
106
+ }
107
+
108
+ const seen = new Set<string>();
109
+
110
+ return combinations.filter((combination) => {
111
+ const key = JSON.stringify(
112
+ Object.entries(combination)
113
+ .map(([name, scopes]) => [name, [...scopes].sort()])
114
+ .sort(),
115
+ );
116
+
117
+ if (seen.has(key)) {
118
+ return false;
119
+ }
120
+
121
+ seen.add(key);
122
+
123
+ return true;
124
+ });
125
+ }
126
+
127
+ /** The scopes of a scheme already in a combination, and another's. */
128
+ function joined(
129
+ present: readonly string[] | undefined,
130
+ added: readonly string[] | undefined,
131
+ ): readonly string[] {
132
+ const scopes = [...(present ?? [])];
133
+
134
+ for (const scope of added ?? []) {
135
+ if (!scopes.includes(scope)) {
136
+ scopes.push(scope);
137
+ }
138
+ }
139
+
140
+ return scopes;
141
+ }
142
+
79
143
  /**
80
144
  * Hands out the `operationId` of every route in one document.
81
145
  *
82
- * Uniqueness is a requirement of the specification and cannot be decided
83
- * one route at a time: the pair a name is built from — the controller and
84
- * the field — is not unique on its own. One instance mounted under two
85
- * prefixes is a documented pattern (`/v1` and `/v2` from the same
86
- * controller), and two controller classes in different modules may simply
87
- * share a class name. Either way the pair repeats while the routes differ.
146
+ * An `operationId` is a contract: a generated client names its methods
147
+ * after them, so an id that moves breaks code nobody reviewing the change
148
+ * sees. The id is therefore always stated or derived from names the
149
+ * author wrote — never from where a route happened to land:
150
+ *
151
+ * | | |
152
+ * | --- | --- |
153
+ * | `docs.operationId` on the route | taken as written |
154
+ * | a route of a named controller | the name and the field — `authRequestCode` |
155
+ * | a route of an unnamed object | the field — `requestCode` |
156
+ * | a route mounted standalone | its method and path — `postAuthCode` |
88
157
  *
89
- * A repeat falls back to what the route is named by when it has no field
90
- * at all — its method and path — because that is derived from the route
91
- * rather than from the order routes were visited in. A numbered name is
92
- * the last resort only, since it would move between documents as routes
93
- * are added, and an `operationId` is what a generated client calls its
94
- * method.
158
+ * Two routes arriving at one id is an error, not something to route
159
+ * around. Falling back to the method and path, or numbering the second
160
+ * one, would hand one of them an id that changes when a path is edited or
161
+ * a route is added — silently, in someone else's SDK. The error names
162
+ * both routes and the two ways out: a controller name of its own, or an
163
+ * id stated on the route. The core already refuses two controllers of
164
+ * one name; what reaches this is two stated ids, or a stated id meeting a
165
+ * derived one.
95
166
  */
96
167
  export interface OperationIds {
97
168
  /** The id for this route, unique within the document being built. */
@@ -99,46 +170,40 @@ export interface OperationIds {
99
170
  }
100
171
 
101
172
  export function operationIds(): OperationIds {
102
- const taken = new Set<string>();
173
+ const taken = new Map<string, RouteTableEntry>();
103
174
 
104
175
  return {
105
176
  take(entry: RouteTableEntry): string {
106
- const preferred = declaredId(entry) ?? derivedId(entry);
107
-
108
- if (!taken.has(preferred)) {
109
- taken.add(preferred);
110
-
111
- return preferred;
112
- }
113
-
114
- const derived = derivedId(entry);
115
-
116
- if (!taken.has(derived)) {
117
- taken.add(derived);
118
-
119
- return derived;
120
- }
121
-
122
- let name = derived;
177
+ const id = idOf(entry);
178
+ const holder = taken.get(id);
123
179
 
124
- for (let attempt = 2; taken.has(name); attempt += 1) {
125
- name = `${derived}${attempt}`;
180
+ if (holder !== undefined) {
181
+ throw new Error(
182
+ `operationId "${id}" is taken by both ${describe(holder)} and ${describe(entry)} — a generated client would get one method for two operations. Give one of them docs.operationId, or its controller a name of its own.`,
183
+ );
126
184
  }
127
185
 
128
- taken.add(name);
186
+ taken.set(id, entry);
129
187
 
130
- return name;
188
+ return id;
131
189
  },
132
190
  };
133
191
  }
134
192
 
135
- /**
136
- * The name the controller gave it: the field, qualified by the controller
137
- * — `usersSetAvatar` for `setAvatar` on `UsersController`.
138
- */
139
- function declaredId(entry: RouteTableEntry): string | undefined {
193
+ /** The id a route asks for, before anything checks it is free. */
194
+ function idOf(entry: RouteTableEntry): string {
195
+ const stated = entry.def.docs?.operationId;
196
+
197
+ if (stated !== undefined) {
198
+ return stated;
199
+ }
200
+
140
201
  if (entry.name === undefined) {
141
- return undefined;
202
+ return derivedId(entry);
203
+ }
204
+
205
+ if (entry.controller === undefined) {
206
+ return entry.name;
142
207
  }
143
208
 
144
209
  const controller = entry.controller.replace(/Controller$/, "");
@@ -146,11 +211,14 @@ function declaredId(entry: RouteTableEntry): string | undefined {
146
211
  return `${lowerFirst(controller)}${capitalize(entry.name)}`;
147
212
  }
148
213
 
214
+ /** How the error above names a route: the one line that finds it. */
215
+ function describe(entry: RouteTableEntry): string {
216
+ return `${entry.method} ${entry.path}`;
217
+ }
218
+
149
219
  /**
150
- * What identifies a route when its field does not: its method and path.
151
- *
152
- * Also the name of a route mounted standalone, which has no field to be
153
- * called after.
220
+ * What a route mounted standalone is named by: it has no field and no
221
+ * controller, and its method and path are what it has.
154
222
  */
155
223
  function derivedId(entry: RouteTableEntry): string {
156
224
  return `${entry.method.toLowerCase()}${entry.path