genoc 0.4.3 → 0.5.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.
Files changed (32) hide show
  1. package/dist/analyzer/analyze.js +69 -27
  2. package/dist/analyzer/path-analyzer.d.ts +3 -1
  3. package/dist/analyzer/path-analyzer.js +2 -1
  4. package/dist/analyzer/types.d.ts +25 -0
  5. package/dist/cli/impl.js +3 -1
  6. package/dist/generator/client-generator.js +64 -4
  7. package/dist/generator/contracts-generator.js +7 -0
  8. package/dist/generator/method-generator.d.ts +9 -1
  9. package/dist/generator/method-generator.js +26 -0
  10. package/dist/parser/spec-reader.js +6 -25
  11. package/dist/parser/version/spec-links.d.ts +55 -0
  12. package/dist/parser/version/spec-links.js +65 -0
  13. package/dist/parser/version/v3.0/validator.d.ts +3 -1
  14. package/dist/parser/version/v3.0/validator.js +23 -23
  15. package/dist/parser/version/v3.1/validator.d.ts +3 -1
  16. package/dist/parser/version/v3.1/validator.js +25 -23
  17. package/dist/parser/version/validate-info.d.ts +9 -0
  18. package/dist/parser/version/validate-info.js +22 -0
  19. package/dist/parser/version/validate-security.d.ts +17 -0
  20. package/dist/parser/version/validate-security.js +218 -0
  21. package/dist/parser/version/version-detector.js +27 -8
  22. package/dist/runtime/defined-errors.d.ts +3 -2
  23. package/dist/runtime/defined-errors.js +6 -3
  24. package/dist/runtime/errors.d.ts +1 -3
  25. package/dist/runtime/errors.js +2 -6
  26. package/dist/runtime/index.d.ts +1 -1
  27. package/dist/runtime/requester.d.ts +67 -0
  28. package/dist/types/openapi.d.ts +5 -4
  29. package/dist/utils/http-methods.d.ts +8 -0
  30. package/dist/utils/http-methods.js +17 -0
  31. package/dist/utils/operation-naming.js +1 -0
  32. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
2
  import { buildSchemaRenameMap, buildTypeJsDoc, sanitizeJsDocText, sanitizeTypeName, toPascalCase, } from '../utils/generator-helpers.js';
3
3
  import { operationEmissions, RESERVED_TYPE_NAMES } from '../utils/operation-naming.js';
4
+ import { escapeStringLiteral } from '../utils/string.js';
4
5
  import { buildDiscriminatorRegistry, renameAwareTypeName } from './discriminator-registry.js';
5
6
  import { analyzePathsDetailed } from './path-analyzer.js';
6
7
  import { SchemaMapper } from './schema-mapper.js';
@@ -151,75 +152,92 @@ function topologicalSort(entries, allNames) {
151
152
  }
152
153
  return sorted;
153
154
  }
154
- function securitySchemeToTsType(scheme) {
155
- const parts = [`type: "${scheme.type}"`];
155
+ /**
156
+ * Build the `key: "value"` fragments of a security scheme object — shared by
157
+ * the contracts TYPE renderer (`'; '` separator) and the client CONST value
158
+ * renderer (`', '` separator).
159
+ */
160
+ function schemeParts(scheme, separator) {
161
+ const parts = [`type: '${escapeStringLiteral(scheme.type)}'`];
156
162
  if (scheme.description) {
157
- parts.push(`description: "${scheme.description}"`);
163
+ parts.push(`description: '${escapeStringLiteral(scheme.description)}'`);
158
164
  }
159
165
  if (scheme.type === 'apiKey') {
160
166
  if (scheme.name)
161
- parts.push(`name: "${scheme.name}"`);
167
+ parts.push(`name: '${escapeStringLiteral(scheme.name)}'`);
162
168
  if (scheme.in)
163
- parts.push(`in: "${scheme.in}"`);
169
+ parts.push(`in: '${escapeStringLiteral(scheme.in)}'`);
164
170
  }
165
171
  if (scheme.type === 'http') {
166
172
  if (scheme.scheme)
167
- parts.push(`scheme: "${scheme.scheme}"`);
168
- if (scheme.bearerFormat)
169
- parts.push(`bearerFormat: "${scheme.bearerFormat}"`);
173
+ parts.push(`scheme: '${escapeStringLiteral(scheme.scheme)}'`);
174
+ if (scheme.bearerFormat) {
175
+ parts.push(`bearerFormat: '${escapeStringLiteral(scheme.bearerFormat)}'`);
176
+ }
170
177
  }
171
178
  if (scheme.type === 'oauth2' && scheme.flows) {
172
179
  const flowParts = [];
173
180
  const flows = scheme.flows;
174
181
  if (flows.implicit) {
175
- flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true)}`);
182
+ flowParts.push(`implicit: ${oAuth2FlowToTs(flows.implicit, true, separator)}`);
176
183
  }
177
184
  if (flows.password) {
178
- flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false)}`);
185
+ flowParts.push(`password: ${oAuth2FlowToTs(flows.password, false, separator)}`);
179
186
  }
180
187
  if (flows.clientCredentials) {
181
- flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false)}`);
188
+ flowParts.push(`clientCredentials: ${oAuth2FlowToTs(flows.clientCredentials, false, separator)}`);
182
189
  }
183
190
  if (flows.authorizationCode) {
184
- flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true)}`);
191
+ flowParts.push(`authorizationCode: ${oAuth2FlowToTs(flows.authorizationCode, true, separator)}`);
185
192
  }
186
- parts.push(`flows: { ${flowParts.join('; ')} }`);
193
+ parts.push(`flows: { ${flowParts.join(separator)} }`);
187
194
  }
188
195
  if (scheme.type === 'openIdConnect' && scheme.openIdConnectUrl) {
189
- parts.push(`openIdConnectUrl: "${scheme.openIdConnectUrl}"`);
196
+ parts.push(`openIdConnectUrl: '${escapeStringLiteral(scheme.openIdConnectUrl)}'`);
190
197
  }
191
- return `{ ${parts.join('; ')} }`;
198
+ return parts;
199
+ }
200
+ function securitySchemeToTsType(scheme) {
201
+ return `{ ${schemeParts(scheme, '; ').join('; ')} }`;
192
202
  }
193
- function oAuth2FlowToTs(flow, hasAuthUrl) {
203
+ /** The scheme object as a value expression — client-file consts (`as const`). */
204
+ function securitySchemeConstExpression(scheme) {
205
+ return `{ ${schemeParts(scheme, ', ').join(', ')} } as const`;
206
+ }
207
+ function oAuth2FlowToTs(flow, hasAuthUrl, separator) {
194
208
  const entries = [];
195
209
  if (hasAuthUrl && flow.authorizationUrl) {
196
- entries.push(`authorizationUrl: "${flow.authorizationUrl}"`);
210
+ entries.push(`authorizationUrl: '${escapeStringLiteral(flow.authorizationUrl)}'`);
197
211
  }
198
212
  if (flow.tokenUrl) {
199
- entries.push(`tokenUrl: "${flow.tokenUrl}"`);
213
+ entries.push(`tokenUrl: '${escapeStringLiteral(flow.tokenUrl)}'`);
200
214
  }
201
215
  if (flow.refreshUrl) {
202
- entries.push(`refreshUrl: "${flow.refreshUrl}"`);
216
+ entries.push(`refreshUrl: '${escapeStringLiteral(flow.refreshUrl)}'`);
203
217
  }
204
218
  const scopeEntries = Object.entries(flow.scopes)
205
- .map(([k, v]) => `"${k}": "${v}"`)
206
- .join('; ');
219
+ .map(([k, v]) => `'${escapeStringLiteral(k)}': '${escapeStringLiteral(v)}'`)
220
+ .join(separator);
207
221
  entries.push(`scopes: { ${scopeEntries} }`);
208
- return `{ ${entries.join('; ')} }`;
222
+ return `{ ${entries.join(separator)} }`;
209
223
  }
210
224
  /**
211
225
  * Translate `components.securitySchemes` into finished `{Name}Auth` type
212
226
  * declarations: sanitize + PascalCase the key, number collisions, render the
213
- * object type text and the description JSDoc. Spec key order preserved.
227
+ * object type text, the client-file const, and the description JSDoc. Spec
228
+ * key order preserved; `$ref` entries resolved through the resolver.
214
229
  */
215
- function analyzeSecuritySchemes(securitySchemes) {
230
+ function analyzeSecuritySchemes(securitySchemes, resolver) {
216
231
  const entries = [];
217
232
  if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
218
233
  return entries;
219
234
  }
220
235
  const usedSecurityTypeNames = new Set();
221
- for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
222
- const baseTypeName = `${sanitizeTypeName(toPascalCase(schemeName))}Auth`;
236
+ const usedConstNames = new Set();
237
+ for (const [schemeName, rawScheme] of Object.entries(securitySchemes)) {
238
+ const scheme = resolver.resolve(rawScheme);
239
+ const pascal = sanitizeTypeName(toPascalCase(schemeName));
240
+ const baseTypeName = `${pascal}Auth`;
223
241
  let typeName = baseTypeName;
224
242
  let n = 2;
225
243
  while (usedSecurityTypeNames.has(typeName)) {
@@ -227,14 +245,36 @@ function analyzeSecuritySchemes(securitySchemes) {
227
245
  n += 1;
228
246
  }
229
247
  usedSecurityTypeNames.add(typeName);
248
+ const baseConstName = `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}Scheme`;
249
+ let constName = baseConstName;
250
+ n = 2;
251
+ while (usedConstNames.has(constName)) {
252
+ constName = `${baseConstName}${n}`;
253
+ n += 1;
254
+ }
255
+ usedConstNames.add(constName);
230
256
  entries.push({
257
+ schemeName,
231
258
  name: typeName,
232
259
  tsType: securitySchemeToTsType(scheme),
233
260
  jsDoc: buildDescriptionJsDoc(scheme.description),
261
+ constName,
262
+ constExpression: securitySchemeConstExpression(scheme),
234
263
  });
235
264
  }
236
265
  return entries;
237
266
  }
267
+ /**
268
+ * Resolve effective `security` requirements into per-call facts: each spec
269
+ * requirement object becomes one AND-list of `{ schemeName, scopes }` calls,
270
+ * `{}` becoming the empty (anonymous-access) alternative. Scheme names are
271
+ * passed through unfiltered; the client generator degrades gracefully if a
272
+ * name has no scheme (unreachable in practice — validation rejects unknown
273
+ * names before analysis runs).
274
+ */
275
+ function analyzeSecurityRequirements(requirements) {
276
+ return requirements.map((requirement) => Object.entries(requirement).map(([schemeName, scopes]) => ({ schemeName, scopes })));
277
+ }
238
278
  /**
239
279
  * Translate `doc.servers` into finished server variable interface
240
280
  * declarations, one per server that declares variables. Names are index-aware
@@ -406,9 +446,11 @@ export function analyze(doc, opts = {}) {
406
446
  if (pathParamTypeNames.size > 0) {
407
447
  op.pathParamTypeNames = [...pathParamTypeNames];
408
448
  }
449
+ const effectiveSecurity = op.security ?? doc.security;
409
450
  operations.push({
410
451
  ...op,
411
452
  contractsLines: [],
453
+ securityRequirements: effectiveSecurity === undefined ? [] : analyzeSecurityRequirements(effectiveSecurity),
412
454
  });
413
455
  }
414
456
  const hasFileUpload = operations.some((op) => op.requestBody?.isMultipart);
@@ -548,7 +590,7 @@ export function analyze(doc, opts = {}) {
548
590
  // Branded types snapshot after every mapSchema call has run, mirroring the
549
591
  // pre-refactor read position at the end of generateContracts.
550
592
  const brandedTypes = Array.from(mapper.getBrandedTypes().values());
551
- const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes);
593
+ const securitySchemeTypes = analyzeSecuritySchemes(doc.components?.securitySchemes, resolver);
552
594
  const serverTypes = analyzeServers(doc.servers);
553
595
  return {
554
596
  specVersion: doc.openapi,
@@ -1,6 +1,6 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
2
  import type { MethodNameStrategy } from '../types/client.js';
3
- import type { OpenAPIDocument, ReferenceObject, SchemaObject } from '../types/openapi.js';
3
+ import type { OpenAPIDocument, ReferenceObject, SchemaObject, SecurityRequirementObject } from '../types/openapi.js';
4
4
  export interface AnalyzedParameter {
5
5
  name: string;
6
6
  in: 'path' | 'query' | 'header' | 'cookie';
@@ -79,6 +79,8 @@ export interface AnalyzedOperation {
79
79
  pathParamTypeNames?: string[];
80
80
  requestBody: AnalyzedRequestBody | undefined;
81
81
  responses: AnalyzedResponse[];
82
+ /** Raw operation-level `security`; `undefined` = inherit the document default. */
83
+ security?: SecurityRequirementObject[];
82
84
  }
83
85
  /**
84
86
  * Analyzer-internal analysis result: the analyzed operations plus the raw
@@ -1,8 +1,8 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
2
  import { isBinaryContentType } from '../utils/generator-helpers.js';
3
+ import { HTTP_METHODS } from '../utils/http-methods.js';
3
4
  import { getOperationTypePrefix } from '../utils/operation-naming.js';
4
5
  import { getMethodName } from './naming.js';
5
- const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
6
6
  /**
7
7
  * Resolve a possibly-$ref-ed schema and check for an exact top-level `format: binary`
8
8
  * match. Top-level only — intentionally no deep-walk into items/allOf/oneOf/anyOf.
@@ -183,6 +183,7 @@ export function analyzePathsDetailed(doc, resolver, strategy = 'path-based') {
183
183
  ...categorized,
184
184
  requestBody: requestBodyResult?.requestBody,
185
185
  responses,
186
+ security: operation.security,
186
187
  });
187
188
  requestBodySchemas.push(requestBodyResult?.rawSchema);
188
189
  }
@@ -20,12 +20,29 @@ export interface TypeDeclaration {
20
20
  * and the description-only JSDoc block ('' when absent).
21
21
  */
22
22
  export interface SecuritySchemeTypeDeclaration {
23
+ /** Spec key under `components.securitySchemes` — the requirement lookup name. */
24
+ schemeName: string;
23
25
  /** Emitted `{Name}Auth` type name (sanitized, numbered on collision). */
24
26
  name: string;
25
27
  /** Rendered object type text — the `securitySchemeToTsType` output. */
26
28
  tsType: string;
27
29
  /** Description JSDoc block; '' when the scheme carries no description. */
28
30
  jsDoc: string;
31
+ /**
32
+ * Client-file const identifier holding the full scheme literal. Inline
33
+ * `scheme:` values in method bodies reference it — one const per scheme
34
+ * caps oauth2-flows bloat at a single site.
35
+ */
36
+ constName: string;
37
+ /** `{ … } as const` — the scheme object as a value expression. */
38
+ constExpression: string;
39
+ }
40
+ /** One scheme invocation inside an effective security requirement. */
41
+ export interface AnalyzedSecurityCall {
42
+ /** Scheme name as declared in `components.securitySchemes` (spec key). */
43
+ schemeName: string;
44
+ /** Requested scopes/roles; empty for non-oauth2/openIdConnect schemes. */
45
+ scopes: string[];
29
46
  }
30
47
  /**
31
48
  * Finished server variable type declaration (contracts Section 1c). Names
@@ -81,6 +98,14 @@ export interface FinishedOperation extends AnalyzedOperation {
81
98
  * emission). Sole fact source for the client generator's FormData appends.
82
99
  */
83
100
  fileUploadProperties?: FileUploadPropertyFact[];
101
+ /**
102
+ * Effective security requirements (operation override ?? document default
103
+ * ?? none): an OR-list of AND-lists. Each call names its scheme by spec
104
+ * key; the client generator resolves it to a `securitySchemeTypes` const.
105
+ * `[]` = public operation (explicitly or by absence). An empty AND-list is
106
+ * the spec's `{}` — "anonymous access acceptable" alternative.
107
+ */
108
+ securityRequirements: AnalyzedSecurityCall[][];
84
109
  }
85
110
  /**
86
111
  * Single-pass analysis model of an OpenAPI document — the ONLY input every
package/dist/cli/impl.js CHANGED
@@ -138,12 +138,14 @@ async function runOneTarget(target, context) {
138
138
  }
139
139
  context.process.stdout.write(`Loading spec from ${target.input}...\n`);
140
140
  const doc = await load(target.input, { proxy: target.proxy });
141
- context.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
142
141
  const override = target.specVersion;
143
142
  if (override !== undefined && override !== '3.0' && override !== '3.1') {
144
143
  throw new Error(`No strategy registered for version: ${override}`);
145
144
  }
145
+ // Resolve the dialect before logging so a missing/malformed `openapi`
146
+ // field is reported (with its spec link) instead of logging "undefined".
146
147
  const profile = resolveVersion(doc, override);
148
+ context.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
147
149
  if (override !== undefined &&
148
150
  target.strictVersion !== false &&
149
151
  profile.detected !== profile.effective) {
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
2
2
  import { clientImportedNames, clientValueImports, getErrorType, getSuccessType, operationEmissions, } from '../utils/operation-naming.js';
3
+ import { escapeStringLiteral } from '../utils/string.js';
3
4
  import { renderContracts } from './contracts-generator.js';
4
5
  import { generateMethod } from './method-generator.js';
5
6
  function collectImportTypes(operations) {
@@ -12,7 +13,34 @@ function collectImportTypes(operations) {
12
13
  // oxlint-disable-next-line unicorn/no-array-sort
13
14
  return [...types].sort();
14
15
  }
15
- function buildClientMethodBody(op) {
16
+ /**
17
+ * Render the `security` option value for a call: `[[call, …], …]` — an
18
+ * OR-list of AND-lists, each call naming its scheme and pointing at the
19
+ * file-level scheme const. Returns `undefined` for public operations (option
20
+ * omitted) and when any scheme name is missing from the declared schemes
21
+ * (unreachable post-validation; degrades to omission rather than emitting a
22
+ * dangling reference).
23
+ */
24
+ function buildSecurityExpression(requirements, schemeConsts) {
25
+ if (requirements.length === 0) {
26
+ return undefined;
27
+ }
28
+ const alternatives = [];
29
+ for (const requirement of requirements) {
30
+ const calls = [];
31
+ for (const call of requirement) {
32
+ const scheme = schemeConsts.get(call.schemeName);
33
+ if (scheme === undefined) {
34
+ return undefined;
35
+ }
36
+ const scopes = call.scopes.map((scope) => `'${escapeStringLiteral(scope)}'`).join(', ');
37
+ calls.push(`{ schemeName: '${escapeStringLiteral(call.schemeName)}', scopes: [${scopes}], scheme: ${scheme.constName} }`);
38
+ }
39
+ alternatives.push(`[${calls.join(', ')}]`);
40
+ }
41
+ return `[${alternatives.join(', ')}]`;
42
+ }
43
+ function buildClientMethodBody(op, schemeConsts) {
16
44
  const successType = getSuccessType(op);
17
45
  const emissions = operationEmissions(op);
18
46
  let urlTemplate = op.path;
@@ -67,6 +95,10 @@ function buildClientMethodBody(op) {
67
95
  if (op.headerParams.length > 0) {
68
96
  opts.push('headers');
69
97
  }
98
+ const securityExpression = buildSecurityExpression(op.securityRequirements, schemeConsts);
99
+ if (securityExpression !== undefined) {
100
+ opts.push(`security: ${securityExpression}`);
101
+ }
70
102
  const lines = [];
71
103
  if (op.requestBody?.isMultipart && op.fileUploadProperties !== undefined) {
72
104
  const bodyRequired = op.requestBody.required;
@@ -123,7 +155,23 @@ function buildClientMethodBody(op) {
123
155
  }
124
156
  return tryCatchLines.join('\n');
125
157
  }
126
- function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
158
+ /**
159
+ * Collect the scheme consts actually referenced by some operation's effective
160
+ * security — spec declaration order preserved. Schemes no operation uses are
161
+ * not emitted (dead weight in the client file).
162
+ */
163
+ function collectUsedSchemeConsts(operations, securitySchemeTypes) {
164
+ const used = new Set();
165
+ for (const op of operations) {
166
+ for (const requirement of op.securityRequirements) {
167
+ for (const call of requirement) {
168
+ used.add(call.schemeName);
169
+ }
170
+ }
171
+ }
172
+ return securitySchemeTypes.filter((scheme) => used.has(scheme.schemeName));
173
+ }
174
+ function buildClientFile(operations, securitySchemeTypes, version, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
127
175
  const lines = [];
128
176
  lines.push(makeHeader(version));
129
177
  const importTypes = collectImportTypes(operations);
@@ -142,6 +190,18 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
142
190
  lines.push('');
143
191
  lines.push(`export { isDefinedError } from '${runtimeImportPath}';`);
144
192
  lines.push(`export type { Requester } from '${runtimeImportPath}';`);
193
+ const schemeConsts = new Map(securitySchemeTypes.map((scheme) => [scheme.schemeName, scheme]));
194
+ const usedSchemes = collectUsedSchemeConsts(operations, securitySchemeTypes);
195
+ if (usedSchemes.length > 0) {
196
+ lines.push('');
197
+ lines.push('/**');
198
+ lines.push(' * Security schemes (from `components.securitySchemes`) referenced by operations below.');
199
+ lines.push(' * Passed to the requester via the `security` option — auth decisions stay in user land.');
200
+ lines.push(' */');
201
+ for (const scheme of usedSchemes) {
202
+ lines.push(`const ${scheme.constName} = ${scheme.constExpression};`);
203
+ }
204
+ }
145
205
  lines.push('');
146
206
  lines.push('export function createClient(requester: Requester) {');
147
207
  lines.push(' return {');
@@ -155,7 +215,7 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
155
215
  .join('\n');
156
216
  lines.push(indentedJsDoc);
157
217
  }
158
- const body = buildClientMethodBody(op);
218
+ const body = buildClientMethodBody(op, schemeConsts);
159
219
  const sig = method.signature;
160
220
  const firstParen = sig.indexOf('(');
161
221
  const closeParenColon = sig.indexOf('): ');
@@ -202,7 +262,7 @@ function buildIndexFile(version) {
202
262
  */
203
263
  export function renderClient(analyzed, config) {
204
264
  const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
205
- const client = buildClientFile(analyzed.operations, analyzed.specVersion, runtimeImportPath);
265
+ const client = buildClientFile(analyzed.operations, analyzed.securitySchemeTypes, analyzed.specVersion, runtimeImportPath);
206
266
  const index = buildIndexFile(analyzed.specVersion);
207
267
  return { client, index };
208
268
  }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
2
2
  import { RUNTIME_CLASS_NAMES } from '../utils/operation-naming.js';
3
+ import { escapeStringLiteral } from '../utils/string.js';
3
4
  /**
4
5
  * Build the import + re-export block for the shared runtime classes.
5
6
  * Generated code re-uses the genoc runtime as the single source of truth
@@ -53,6 +54,12 @@ export function renderContracts(analyzed, runtimeImportPath = DEFAULT_RUNTIME_IM
53
54
  lines.push('');
54
55
  lines.push(`export type ${entry.name} = ${entry.tsType};`);
55
56
  }
57
+ const nameMembers = analyzed.securitySchemeTypes
58
+ .map((entry) => `'${escapeStringLiteral(entry.schemeName)}'`)
59
+ .join(' | ');
60
+ lines.push('');
61
+ lines.push('/** Names of the security schemes declared in `components.securitySchemes` (spec keys). */');
62
+ lines.push(`export type SecuritySchemeName = ${nameMembers};`);
56
63
  if (analyzed.securitySchemeTypes.length > 1) {
57
64
  const unionMembers = analyzed.securitySchemeTypes.map((entry) => entry.name).join(' | ');
58
65
  lines.push('');
@@ -1,9 +1,17 @@
1
1
  import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ import type { FinishedOperation } from '../analyzer/types.js';
2
3
  import type { GeneratedMethod } from '../types/client.js';
4
+ /**
5
+ * Method rendering input: an analyzed operation plus the finished security
6
+ * facts when available (`analyze()` output; bare `AnalyzedOperation`s from
7
+ * path analysis render without the `@security` tag).
8
+ */
9
+ type MethodOperation = AnalyzedOperation & Partial<Pick<FinishedOperation, 'securityRequirements'>>;
3
10
  /**
4
11
  * Generate a client method from an analyzed OpenAPI operation.
5
12
  *
6
13
  * @param op - The analyzed operation
7
14
  * @returns Generated method with name, JSDoc, and signature
8
15
  */
9
- export declare function generateMethod(op: AnalyzedOperation): GeneratedMethod;
16
+ export declare function generateMethod(op: MethodOperation): GeneratedMethod;
17
+ export {};
@@ -94,6 +94,13 @@ function buildJsDoc(op) {
94
94
  }
95
95
  lines.push(` * @param body — request body`);
96
96
  }
97
+ const securitySummary = renderSecuritySummary(op.securityRequirements);
98
+ if (securitySummary !== undefined) {
99
+ if (lines.length > 0) {
100
+ lines.push(' *');
101
+ }
102
+ lines.push(` * @security ${sanitizeJsDocText(securitySummary)}`);
103
+ }
97
104
  if (op.tags && op.tags.length > 0) {
98
105
  if (lines.length > 0) {
99
106
  lines.push(' *');
@@ -122,6 +129,25 @@ function buildJsDoc(op) {
122
129
  }
123
130
  return `/**\n${lines.join('\n')}\n */`;
124
131
  }
132
+ /**
133
+ * Render the `@security` tag text: OR-joined alternatives, AND-joined schemes
134
+ * within one alternative (parenthesized), scopes as `name[scope, …]`. An
135
+ * empty alternative is the spec's `{}` — rendered as `anonymous`. Returns
136
+ * `undefined` for public operations (no tag emitted).
137
+ */
138
+ function renderSecuritySummary(requirements) {
139
+ if (requirements === undefined || requirements.length === 0) {
140
+ return undefined;
141
+ }
142
+ const alternatives = requirements.map((requirement) => {
143
+ const calls = requirement.map((call) => call.scopes.length > 0 ? `${call.schemeName}[${call.scopes.join(', ')}]` : call.schemeName);
144
+ if (calls.length === 0) {
145
+ return 'anonymous';
146
+ }
147
+ return calls.length > 1 ? `(${calls.join(' AND ')})` : (calls[0] ?? '');
148
+ });
149
+ return alternatives.join(' OR ');
150
+ }
125
151
  /**
126
152
  * Generate a client method from an analyzed OpenAPI operation.
127
153
  *
@@ -5,23 +5,6 @@ import { fetchSpec } from '../utils/proxy.js';
5
5
  import { isUrl } from '../utils/url.js';
6
6
  const VALID_EXTENSIONS = ['.json', '.yaml', '.yml'];
7
7
  const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
8
- function assertSupportedVersion(doc) {
9
- if (typeof doc !== 'object' ||
10
- doc === null ||
11
- !('openapi' in doc) ||
12
- typeof doc.openapi !== 'string') {
13
- throw new Error("Invalid spec: missing or invalid 'openapi' field.");
14
- }
15
- const version = doc.openapi;
16
- // Check for valid version format
17
- if (!version.match(/^\d+\.\d+(\.\d+)?$/)) {
18
- throw new Error(`Invalid OpenAPI version format: ${version}`);
19
- }
20
- // Check that major version is 3 (but don't restrict minor versions)
21
- if (!version.startsWith('3.')) {
22
- throw new Error(`Unsupported OpenAPI version: ${version}. Supported versions: 3.0, 3.1, 3.2`);
23
- }
24
- }
25
8
  function parseContent(content, ext) {
26
9
  if (ext === '.json') {
27
10
  try {
@@ -69,10 +52,10 @@ export async function loadFromFile(filePath) {
69
52
  throw new Error(`Failed to load spec from file: ${err.message}`, { cause: err });
70
53
  }
71
54
  const doc = parseContent(content, ext);
72
- if (typeof doc !== 'object' || doc === null) {
73
- throw new Error('Failed to parse: spec is not an object.');
74
- }
75
- assertSupportedVersion(doc);
55
+ // Version/dialect rules — including whether the parsed root is even an
56
+ // OpenAPI document — belong to the version module, which resolves the
57
+ // dialect and links spec violations to the specification. The cast is safe
58
+ // for the same I/O reason as above: callers validate via resolveVersion.
76
59
  return doc;
77
60
  }
78
61
  export async function loadFromUrl(url, opts) {
@@ -103,10 +86,8 @@ export async function loadFromUrl(url, opts) {
103
86
  throw new Error(`Failed to parse spec from URL as JSON or YAML: ${yamlErr.message}`, { cause: yamlErr });
104
87
  }
105
88
  }
106
- if (typeof doc !== 'object' || doc === null) {
107
- throw new Error('Failed to parse: spec is not an object.');
108
- }
109
- assertSupportedVersion(doc);
89
+ // Version/dialect rules — including document-rootedness — belong to the
90
+ // version module (see loadFromFile).
110
91
  return doc;
111
92
  }
112
93
  export async function load(source, opts) {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Single source of links from spec-violation errors to the section of the
3
+ * OpenAPI specification that states the rule being violated.
4
+ *
5
+ * Unsupported-feature errors (Swagger 2.0, OpenAPI 3.2, external `$ref`s)
6
+ * never use this module: the document is valid and the error cites a genoc
7
+ * limitation, not a specification rule.
8
+ */
9
+ /** Latest patch release per dialect. */
10
+ export declare const SPEC_PAGE: Record<'3.0' | '3.1', string>;
11
+ /**
12
+ * One anchor per violated rule, shared by both dialect pages. A wrong anchor
13
+ * is a one-line fix here; no automated anchor checking exists.
14
+ */
15
+ export declare const SPEC_ANCHORS: {
16
+ readonly 'document-root': "openapi-object";
17
+ readonly 'openapi-field': "oas-version";
18
+ readonly 'info-object': "info-object";
19
+ readonly 'info-title': "info-title";
20
+ readonly 'info-version': "info-version";
21
+ readonly 'paths-object': "paths-object";
22
+ readonly 'path-item-object': "path-item-object";
23
+ readonly 'components-security-schemes': "security-scheme-object";
24
+ readonly 'components-schemas': "components-schemas";
25
+ readonly 'schema-object': "schema-object";
26
+ readonly 'security-scheme-object': "security-scheme-object-0";
27
+ readonly 'security-scheme-type': "security-scheme-type";
28
+ readonly 'security-scheme-name': "security-scheme-name";
29
+ readonly 'security-scheme-in': "security-scheme-in";
30
+ readonly 'security-scheme-scheme': "security-scheme-scheme";
31
+ readonly 'security-scheme-flows': "security-scheme-flows";
32
+ readonly 'security-scheme-open-id-connect-url': "security-scheme-open-id-connect-url";
33
+ readonly 'oauth-flow-object': "oauth-flow-object";
34
+ readonly 'oauth-flow-scopes': "oauth-flow-scopes";
35
+ readonly 'reference-object': "reference-object";
36
+ readonly 'security-requirement-object': "security-requirement-object";
37
+ };
38
+ export type SpecLinkKey = keyof typeof SPEC_ANCHORS;
39
+ export declare function specLinkUrl(version: '3.0' | '3.1', key: SpecLinkKey): string;
40
+ /**
41
+ * Renders a spec-violation message with its link line appended on its own
42
+ * line, indented four spaces so it aligns under the ` - ` bullet that
43
+ * `validateSpec` puts each message in.
44
+ */
45
+ export declare function specViolation(version: '3.0' | '3.1', key: SpecLinkKey, message: string): string;
46
+ /**
47
+ * Detection error for a document that violates the specification itself
48
+ * (as opposed to an unsupported-but-valid dialect). Carries the anchor key
49
+ * so `resolveVersion` can render the link against the dialect being applied.
50
+ */
51
+ export declare class SpecViolationError extends Error {
52
+ /** Which specification section states the violated rule. */
53
+ linkKey: SpecLinkKey;
54
+ constructor(message: string, linkKey: SpecLinkKey);
55
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Single source of links from spec-violation errors to the section of the
3
+ * OpenAPI specification that states the rule being violated.
4
+ *
5
+ * Unsupported-feature errors (Swagger 2.0, OpenAPI 3.2, external `$ref`s)
6
+ * never use this module: the document is valid and the error cites a genoc
7
+ * limitation, not a specification rule.
8
+ */
9
+ /** Latest patch release per dialect. */
10
+ export const SPEC_PAGE = {
11
+ '3.0': 'https://spec.openapis.org/oas/v3.0.4.html',
12
+ '3.1': 'https://spec.openapis.org/oas/v3.1.1.html',
13
+ };
14
+ /**
15
+ * One anchor per violated rule, shared by both dialect pages. A wrong anchor
16
+ * is a one-line fix here; no automated anchor checking exists.
17
+ */
18
+ export const SPEC_ANCHORS = {
19
+ 'document-root': 'openapi-object',
20
+ 'openapi-field': 'oas-version',
21
+ 'info-object': 'info-object',
22
+ 'info-title': 'info-title',
23
+ 'info-version': 'info-version',
24
+ 'paths-object': 'paths-object',
25
+ 'path-item-object': 'path-item-object',
26
+ 'components-security-schemes': 'security-scheme-object',
27
+ 'components-schemas': 'components-schemas',
28
+ 'schema-object': 'schema-object',
29
+ 'security-scheme-object': 'security-scheme-object-0',
30
+ 'security-scheme-type': 'security-scheme-type',
31
+ 'security-scheme-name': 'security-scheme-name',
32
+ 'security-scheme-in': 'security-scheme-in',
33
+ 'security-scheme-scheme': 'security-scheme-scheme',
34
+ 'security-scheme-flows': 'security-scheme-flows',
35
+ 'security-scheme-open-id-connect-url': 'security-scheme-open-id-connect-url',
36
+ 'oauth-flow-object': 'oauth-flow-object',
37
+ 'oauth-flow-scopes': 'oauth-flow-scopes',
38
+ 'reference-object': 'reference-object',
39
+ 'security-requirement-object': 'security-requirement-object',
40
+ };
41
+ export function specLinkUrl(version, key) {
42
+ return `${SPEC_PAGE[version]}#${SPEC_ANCHORS[key]}`;
43
+ }
44
+ /**
45
+ * Renders a spec-violation message with its link line appended on its own
46
+ * line, indented four spaces so it aligns under the ` - ` bullet that
47
+ * `validateSpec` puts each message in.
48
+ */
49
+ export function specViolation(version, key, message) {
50
+ return `${message}\n ${specLinkUrl(version, key)}`;
51
+ }
52
+ /**
53
+ * Detection error for a document that violates the specification itself
54
+ * (as opposed to an unsupported-but-valid dialect). Carries the anchor key
55
+ * so `resolveVersion` can render the link against the dialect being applied.
56
+ */
57
+ export class SpecViolationError extends Error {
58
+ /** Which specification section states the violated rule. */
59
+ linkKey;
60
+ constructor(message, linkKey) {
61
+ super(message);
62
+ this.name = 'SpecViolationError';
63
+ this.linkKey = linkKey;
64
+ }
65
+ }